Compare commits

..

11 commits

Author SHA1 Message Date
Will Charczuk
62b1e2c499 should not use unkeyed fields anyway. 2017-03-05 16:53:21 -08:00
Will Charczuk
8b34cb3bd7 sanity check tests. 2017-03-05 16:52:34 -08:00
Will Charczuk
10950a3bf2 color tests etc. 2017-03-05 16:23:11 -08:00
Will Charczuk
d9b5269579 updated output.png 2017-03-05 14:25:13 -08:00
Will Charczuk
412b25feb4 testing auto coloring 2017-03-05 14:24:35 -08:00
Will Charczuk
68cc6a95d3 missed a couple series validations 2017-03-05 14:03:11 -08:00
Will Charczuk
046daf94fb tweaks 2017-03-05 00:59:10 -08:00
Will Charczuk
2c9a9218e5 adding output 2017-03-04 18:19:40 -08:00
Will Charczuk
feef494764 removing debugging printf 2017-03-04 18:17:59 -08:00
Will Charczuk
fb0040390c updating comment 2017-03-04 17:50:57 -08:00
Will Charczuk
9c65a94050 works more or less 2017-03-04 17:42:10 -08:00
229 changed files with 4351 additions and 8834 deletions

View file

@ -1,33 +0,0 @@
name: "Continuous Integration"
on:
workflow_dispatch:
push:
branches: [ main ]
paths: [ "*.go" ]
pull_request:
branches: [ main ]
paths: [ "*.go" ]
jobs:
ci:
name: "Tests"
runs-on: ubuntu-latest
env:
GOOS: "linux"
GOARCH: "amd64"
GO111MODULE: "on"
CGO_ENABLED: "0"
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Check out go-incr
uses: actions/checkout@v3
- name: Run all tests
run: go test ./...

19
.gitignore vendored
View file

@ -1,20 +1 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Other
.vscode
.DS_Store
coverage.html
.idea

13
.travis.yml Normal file
View file

@ -0,0 +1,13 @@
language: go
go:
- 1.6.2
sudo: false
before_script:
- go get -u github.com/blendlabs/go-assert
- go get ./...
script:
- go test ./...

View file

@ -1 +0,0 @@
29.02

View file

@ -1,7 +1,6 @@
MIT License
Copyright (c) 2016 William Charczuk.
Copyright (c) 2024 Zeni Kim.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,10 +1,9 @@
all: new-install test
new-install:
@go get -v -u ./...
generate:
@go generate ./...
all: test
test:
@go test ./...
@go test ./...
cover:
@go test -short -covermode=set -coverprofile=profile.cov
@go tool cover -html=profile.cov
@rm profile.cov

View file

@ -1,4 +0,0 @@
go-sdk:
excludeFiles: [ "*_test.go" ]
importsContain: [ github.com/blend/go-sdk/* ]
description: "please don't use go-sdk in this repo"

View file

@ -1,55 +1,55 @@
go-chart
========
[![Build Status](https://travis-ci.org/wcharczuk/go-chart.svg?branch=master)](https://travis-ci.org/wcharczuk/go-chart)[![Go Report Card](https://goreportcard.com/badge/github.com/wcharczuk/go-chart)](https://goreportcard.com/report/github.com/wcharczuk/go-chart)
This project starts from a full copy from [https://git.smarteching.com/zeni/go-chart](https://git.smarteching.com/zeni/go-chart). 28 Oct 2024.
Package `chart` is a very simple golang native charting library that supports timeseries and continuous
line charts.
-
The v1.0 release has been tagged so things should be more or less stable, if something changes please log an issue.
Master should now be on the v3.x codebase, which overhauls the api significantly. Per usual, see `examples` for more information.
Master should now be on the v2.x codebase, which brings a couple new features and better handling of basics like axes labeling etc. Per usual, see `_examples` for more information.
# Installation
To install `chart` run the following:
```bash
> go get git.smarteching.com/zeni/go-chart/v2@latest
> go get -u github.com/wcharczuk/go-chart
```
Most of the components are interchangeable so feel free to crib whatever you want.
Most of the components are interchangeable so feel free to crib whatever you want.
# Output Examples
# Output Examples
Spark Lines:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/tvix_ltm.png)
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/tvix_ltm.png)
Single axis:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/goog_ltm.png)
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/goog_ltm.png)
Two axis:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/two_axis.png)
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/two_axis.png)
# Other Chart Types
Pie Chart:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/pie_chart.png)
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/pie_chart.png)
The code for this chart can be found in `examples/pie_chart/main.go`.
The code for this chart can be found in `_examples/pie_chart/main.go`.
Stacked Bar:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/stacked_bar.png)
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/stacked_bar.png)
The code for this chart can be found in `examples/stacked_bar/main.go`.
The code for this chart can be found in `_examples/stacked_bar/main.go`.
# Code Examples
Actual chart configurations and examples can be found in the `./examples/` directory. They are simple CLI programs that write to `output.png` (they are also updated with `go generate`.
If folder ends in "web", has web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output.
Actual chart configurations and examples can be found in the `./_examples/` directory. They are web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output.
# Usage
@ -61,7 +61,7 @@ import (
...
"bytes"
...
"git.smarteching.com/zeni/go-chart/v2" //exposes "chart"
"github.com/wcharczuk/go-chart" //exposes "chart"
)
graph := chart.Chart{
@ -83,7 +83,8 @@ Here, we have a single series with x range values as float64s, rendered to a PNG
# API Overview
Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default.
Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default. One complication here
is any object's root `chart.Style` object (i.e named `Style`) and the `Show` property specifically, if any other property is set and the `Show` property is unset, it is assumed to be it's default value of `False`.
The best way to see the api in action is to look at the examples in the `./_examples/` directory.
@ -95,4 +96,4 @@ The goal with the API itself is to have the "zero value be useful", and to requi
# Contributions
Contributions are welcome though this library is in a holding pattern for the forseable future.
This library is super early but contributions are welcome.

View file

@ -1,147 +0,0 @@
aliceblue #f0f8ff 240,248,255
antiquewhite #faebd7 250,235,215
aqua #00ffff 0,255,255
aquamarine #7fffd4 127,255,212
azure #f0ffff 240,255,255
beige #f5f5dc 245,245,220
bisque #ffe4c4 255,228,196
black #000000 0,0,0
blanchedalmond #ffebcd 255,235,205
blue #0000ff 0,0,255
blueviolet #8a2be2 138,43,226
brown #a52a2a 165,42,42
burlywood #deb887 222,184,135
cadetblue #5f9ea0 95,158,160
chartreuse #7fff00 127,255,0
chocolate #d2691e 210,105,30
coral #ff7f50 255,127,80
cornflowerblue #6495ed 100,149,237
cornsilk #fff8dc 255,248,220
crimson #dc143c 220,20,60
cyan #00ffff 0,255,255
darkblue #00008b 0,0,139
darkcyan #008b8b 0,139,139
darkgoldenrod #b8860b 184,134,11
darkgray #a9a9a9 169,169,169
darkgreen #006400 0,100,0
darkgrey #a9a9a9 169,169,169
darkkhaki #bdb76b 189,183,107
darkmagenta #8b008b 139,0,139
darkolivegreen #556b2f 85,107,47
darkorange #ff8c00 255,140,0
darkorchid #9932cc 153,50,204
darkred #8b0000 139,0,0
darksalmon #e9967a 233,150,122
darkseagreen #8fbc8f 143,188,143
darkslateblue #483d8b 72,61,139
darkslategray #2f4f4f 47,79,79
darkslategrey #2f4f4f 47,79,79
darkturquoise #00ced1 0,206,209
darkviolet #9400d3 148,0,211
deeppink #ff1493 255,20,147
deepskyblue #00bfff 0,191,255
dimgray #696969 105,105,105
dimgrey #696969 105,105,105
dodgerblue #1e90ff 30,144,255
firebrick #b22222 178,34,34
floralwhite #fffaf0 255,250,240
forestgreen #228b22 34,139,34
fuchsia #ff00ff 255,0,255
gainsboro #dcdcdc 220,220,220
ghostwhite #f8f8ff 248,248,255
gold #ffd700 255,215,0
goldenrod #daa520 218,165,32
gray #808080 128,128,128
green #008000 0,128,0
greenyellow #adff2f 173,255,47
grey #808080 128,128,128
honeydew #f0fff0 240,255,240
hotpink #ff69b4 255,105,180
indianred #cd5c5c 205,92,92
indigo #4b0082 75,0,130
ivory #fffff0 255,255,240
khaki #f0e68c 240,230,140
lavender #e6e6fa 230,230,250
lavenderblush #fff0f5 255,240,245
lawngreen #7cfc00 124,252,0
lemonchiffon #fffacd 255,250,205
lightblue #add8e6 173,216,230
lightcoral #f08080 240,128,128
lightcyan #e0ffff 224,255,255
lightgoldenrodyellow #fafad2 250,250,210
lightgray #d3d3d3 211,211,211
lightgreen #90ee90 144,238,144
lightgrey #d3d3d3 211,211,211
lightpink #ffb6c1 255,182,193
lightsalmon #ffa07a 255,160,122
lightseagreen #20b2aa 32,178,170
lightskyblue #87cefa 135,206,250
lightslategray #778899 119,136,153
lightslategrey #778899 119,136,153
lightsteelblue #b0c4de 176,196,222
lightyellow #ffffe0 255,255,224
lime #00ff00 0,255,0
limegreen #32cd32 50,205,50
linen #faf0e6 250,240,230
magenta #ff00ff 255,0,255
maroon #800000 128,0,0
mediumaquamarine #66cdaa 102,205,170
mediumblue #0000cd 0,0,205
mediumorchid #ba55d3 186,85,211
mediumpurple #9370db 147,112,219
mediumseagreen #3cb371 60,179,113
mediumslateblue #7b68ee 123,104,238
mediumspringgreen #00fa9a 0,250,154
mediumturquoise #48d1cc 72,209,204
mediumvioletred #c71585 199,21,133
midnightblue #191970 25,25,112
mintcream #f5fffa 245,255,250
mistyrose #ffe4e1 255,228,225
moccasin #ffe4b5 255,228,181
navajowhite #ffdead 255,222,173
navy #000080 0,0,128
oldlace #fdf5e6 253,245,230
olive #808000 128,128,0
olivedrab #6b8e23 107,142,35
orange #ffa500 255,165,0
orangered #ff4500 255,69,0
orchid #da70d6 218,112,214
palegoldenrod #eee8aa 238,232,170
palegreen #98fb98 152,251,152
paleturquoise #afeeee 175,238,238
palevioletred #db7093 219,112,147
papayawhip #ffefd5 255,239,213
peachpuff #ffdab9 255,218,185
peru #cd853f 205,133,63
pink #ffc0cb 255,192,203
plum #dda0dd 221,160,221
powderblue #b0e0e6 176,224,230
purple #800080 128,0,128
red #ff0000 255,0,0
rosybrown #bc8f8f 188,143,143
royalblue #4169e1 65,105,225
saddlebrown #8b4513 139,69,19
salmon #fa8072 250,128,114
sandybrown #f4a460 244,164,96
seagreen #2e8b57 46,139,87
seashell #fff5ee 255,245,238
sienna #a0522d 160,82,45
silver #c0c0c0 192,192,192
skyblue #87ceeb 135,206,235
slateblue #6a5acd 106,90,205
slategray #708090 112,128,144
slategrey #708090 112,128,144
snow #fffafa 255,250,250
springgreen #00ff7f 0,255,127
steelblue #4682b4 70,130,180
tan #d2b48c 210,180,140
teal #008080 0,128,128
thistle #d8bfd8 216,191,216
tomato #ff6347 255,99,71
turquoise #40e0d0 64,224,208
violet #ee82ee 238,130,238
wheat #f5deb3 245,222,179
white #ffffff 255,255,255
whitesmoke #f5f5f5 245,245,245
yellow #ffff00 255,255,0
yellowgreen #9acd32 154,205,50

View file

@ -1,14 +1,13 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add an `Annotation` series, which is a special type of series that
draws annotation labels at given X and Y values (as translated by their respective ranges).
@ -38,7 +37,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
chart "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -17,14 +15,19 @@ func main() {
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "The XAxis",
Style: chart.Style{
Show: true, //enables / displays the x-axis
},
},
YAxis: chart.YAxis{
Name: "The YAxis",
Style: chart.Style{
Show: true, //enables / displays the y-axis
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
@ -34,7 +37,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

BIN
_examples/axes/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
chart "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -16,9 +14,20 @@ func main() {
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "The XAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
YAxis: chart.YAxis{
Name: "The YAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
@ -28,7 +37,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,26 +1,26 @@
package main
//go:generate go run main.go
import (
"fmt"
"log"
"net/http"
"os"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.BarChart{
Title: "Test Bar Chart",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
sbc := chart.BarChart{
Height: 512,
BarWidth: 60,
XAxis: chart.Style{
Show: true,
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Bars: []chart.Value{
{Value: 5.25, Label: "Blue"},
{Value: 4.88, Label: "Green"},
@ -33,11 +33,10 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
err := sbc.Render(chart.PNG, res)
if err != nil {
fmt.Printf("Error rendering chart: %v\n", err)
}
}
func port() string {

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

43
_examples/basic/main.go Normal file
View file

@ -0,0 +1,43 @@
package main
import (
"log"
"net/http"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func drawChartWide(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Width: 1920, //this overrides the default.
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/wide", drawChartWide)
log.Fatal(http.ListenAndServe(":8080", nil))
}

BIN
_examples/basic/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,15 +1,13 @@
package main
//go:generate go run main.go
import (
"fmt"
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we use a custom `ValueFormatter` for the y axis, letting us specify how to format text of the y-axis ticks.
You can also do this for the x-axis, or the secondary y-axis.
@ -18,6 +16,9 @@ func main() {
graph := chart.Chart{
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%0.6f", vf)
@ -32,7 +33,12 @@ func main() {
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,74 @@
package main
import (
"net/http"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 50,
Left: 25,
Right: 25,
Bottom: 10,
},
FillColor: drawing.ColorFromHex("efefef"),
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.Sequence.Random(100.0, 256.0),
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func drawChartDefault(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Background: chart.Style{
FillColor: drawing.ColorFromHex("efefef"),
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.Sequence.Random(100.0, 256.0),
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/default", drawChartDefault)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we set a custom range for the y-axis, overriding the automatic range generation.
Note: the chart will still generate the ticks automatically based on the custom range, so the intervals may be a bit weird.
@ -16,6 +14,9 @@ func main() {
graph := chart.Chart{
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{
Min: 0.0,
Max: 10.0,
@ -28,7 +29,12 @@ func main() {
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,15 +1,13 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we set some custom colors for the series and the chart background and canvas.
*/
@ -23,6 +21,7 @@ func main() {
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
Show: true, //note; if we set ANY other properties, we must set this to true.
StrokeColor: drawing.ColorRed, // will supercede defaults
FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults
},
@ -32,7 +31,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we set a custom set of ticks to use for the y-axis. It can be (almost) whatever you want, including some custom labels for ticks.
Custom ticks will supercede a custom range, which will supercede automatic generation based on series values.
@ -16,17 +14,20 @@ func main() {
graph := chart.Chart{
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{
Min: 0.0,
Max: 4.0,
},
Ticks: []chart.Tick{
{Value: 0.0, Label: "0.00"},
{Value: 2.0, Label: "2.00"},
{Value: 4.0, Label: "4.00"},
{Value: 6.0, Label: "6.00"},
{Value: 8.0, Label: "Eight"},
{Value: 10.0, Label: "Ten"},
{0.0, "0.00"},
{2.0, "2.00"},
{4.0, "4.00"},
{6.0, "6.00"},
{8.0, "Eight"},
{10.0, "Ten"},
},
},
Series: []chart.Series{
@ -36,7 +37,12 @@ func main() {
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -21,12 +19,18 @@ func main() {
graph := chart.Chart{
Height: 500,
Width: 500,
XAxis: chart.XAxis{
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
/*Range: &chart.ContinuousRange{
Descending: true,
},*/
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{
Descending: true,
},
@ -34,6 +38,7 @@ func main() {
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
@ -43,7 +48,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

View file

@ -4,7 +4,7 @@ import (
"fmt"
"log"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
chart "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add a `Renderable` or a custom component to the `Elements` array.
@ -17,6 +15,12 @@ func main() {
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{Show: true},
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
},
Background: chart.Style{
Padding: chart.Box{
Top: 20,
@ -37,7 +41,11 @@ func main() {
chart.Legend(&graph),
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

BIN
_examples/legend/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,14 +1,12 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add a `Renderable` or a custom component to the `Elements` array.
@ -17,6 +15,12 @@ func main() {
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{Show: true},
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
},
Background: chart.Style{
Padding: chart.Box{
Top: 20,
@ -97,7 +101,11 @@ func main() {
chart.LegendLeft(&graph),
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -1,24 +1,22 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
chart "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument.
InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
*/
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements.
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
}
// note we create a LinearRegressionSeries series by assignin the inner series.
@ -34,7 +32,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -0,0 +1,44 @@
package main
import (
"net/http"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
start := chart.Date.Date(2016, 7, 01, chart.Date.Eastern())
end := chart.Date.Date(2016, 07, 21, chart.Date.Eastern())
xv := chart.Sequence.MarketHours(start, end, chart.NYSEOpen, chart.NYSEClose, chart.Date.IsNYSEHoliday)
yv := chart.Sequence.RandomWithAverage(len(xv), 200, 10)
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.StyleShow(),
TickPosition: chart.TickPositionBetweenTicks,
ValueFormatter: chart.TimeHourValueFormatter,
Range: &chart.MarketHoursRange{
MarketOpen: chart.NYSEOpen,
MarketClose: chart.NYSEClose,
HolidayProvider: chart.Date.IsNYSEHoliday,
},
},
YAxis: chart.YAxis{
Style: chart.StyleShow(),
},
Series: []chart.Series{
chart.TimeSeries{
XValues: xv,
YValues: yv,
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,22 +1,21 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(),
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(50).WithMax(150)}.Values(),
XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.Sequence.RandomWithAverage(100, 100, 50),
}
minSeries := &chart.MinSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateGray,
StrokeDashArray: []float64{5.0, 5.0},
},
@ -25,6 +24,7 @@ func main() {
maxSeries := &chart.MaxSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateGray,
StrokeDashArray: []float64{5.0, 5.0},
},
@ -35,27 +35,35 @@ func main() {
Width: 1920,
Height: 1080,
YAxis: chart.YAxis{
Name: "Random Values",
Name: "Random Values",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
Range: &chart.ContinuousRange{
Min: 25,
Max: 175,
},
},
XAxis: chart.XAxis{
Name: "Random Other Values",
Name: "Random Other Values",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
Series: []chart.Series{
mainSeries,
minSeries,
maxSeries,
chart.LastValueAnnotationSeries(minSeries),
chart.LastValueAnnotationSeries(maxSeries),
chart.LastValueAnnotation(minSeries),
chart.LastValueAnnotation(maxSeries),
},
}
graph.Elements = []chart.Renderable{chart.Legend(&graph)}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -5,7 +5,7 @@ import (
"log"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
@ -30,26 +30,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}
}
func drawChartRegression(res http.ResponseWriter, req *http.Request) {
pie := chart.PieChart{
Width: 512,
Height: 512,
Values: []chart.Value{
{Value: 5, Label: "Blue"},
{Value: 2, Label: "Two"},
{Value: 1, Label: "One"},
},
}
res.Header().Set("Content-Type", chart.ContentTypeSVG)
err := pie.Render(chart.SVG, res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/reg", drawChartRegression)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,135 @@
package main
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/wcharczuk/go-chart"
)
func parseInt(str string) int {
v, _ := strconv.Atoi(str)
return v
}
func parseFloat64(str string) float64 {
v, _ := strconv.ParseFloat(str, 64)
return v
}
func readData() ([]time.Time, []float64) {
var xvalues []time.Time
var yvalues []float64
err := chart.File.ReadByLines("requests.csv", func(line string) {
parts := strings.Split(line, ",")
year := parseInt(parts[0])
month := parseInt(parts[1])
day := parseInt(parts[2])
hour := parseInt(parts[3])
elapsedMillis := parseFloat64(parts[4])
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
yvalues = append(yvalues, elapsedMillis)
})
if err != nil {
fmt.Println(err.Error())
}
return xvalues, yvalues
}
func releases() []chart.GridLine {
return []chart.GridLine{
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))},
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
}
}
func drawChart(res http.ResponseWriter, req *http.Request) {
xvalues, yvalues := readData()
mainSeries := chart.TimeSeries{
Name: "Prod Request Timings",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorBlue,
FillColor: chart.ColorBlue.WithAlpha(100),
},
XValues: xvalues,
YValues: yvalues,
}
linreg := &chart.LinearRegressionSeries{
Name: "Linear Regression",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateBlue,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
sma := &chart.SMASeries{
Name: "SMA",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorRed,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
graph := chart.Chart{
Width: 1280,
Height: 720,
Background: chart.Style{
Padding: chart.Box{
Top: 50,
},
},
YAxis: chart.YAxis{
Name: "Elapsed Millis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
TickStyle: chart.Style{
TextRotationDegrees: 45.0,
},
ValueFormatter: func(v interface{}) string {
return fmt.Sprintf("%d ms", int(v.(float64)))
},
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
ValueFormatter: chart.TimeHourValueFormatter,
GridMajorStyle: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateGray,
StrokeWidth: 1.0,
},
GridLines: releases(),
},
Series: []chart.Series{
mainSeries,
linreg,
chart.LastValueAnnotation(linreg),
sma,
chart.LastValueAnnotation(sma),
},
}
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

View file

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

80
_examples/scatter/main.go Normal file
View file

@ -0,0 +1,80 @@
package main
import (
"log"
"net/http"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeWidth: chart.Disabled,
DotWidth: 3,
},
XValues: chart.Sequence.Random(32, 1024),
YValues: chart.Sequence.Random(32, 1024),
},
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeWidth: chart.Disabled,
DotWidth: 5,
},
XValues: chart.Sequence.Random(16, 1024),
YValues: chart.Sequence.Random(16, 1024),
},
chart.ContinuousSeries{
Style: chart.Style{
Show: true,
StrokeWidth: chart.Disabled,
DotWidth: 7,
},
XValues: chart.Sequence.Random(8, 1024),
YValues: chart.Sequence.Random(8, 1024),
},
},
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
if err != nil {
log.Println(err.Error())
}
}
func unit(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Height: 50,
Width: 50,
Canvas: chart.Style{
Padding: chart.Box{IsSet: true},
},
Background: chart.Style{
Padding: chart.Box{IsSet: true},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Sequence.Float64(0, 4, 1),
YValues: chart.Sequence.Float64(0, 4, 1),
},
},
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
if err != nil {
log.Println(err.Error())
}
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/unit", unit)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,42 @@
package main
import (
"net/http"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument.
InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
*/
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
}
// note we create a SimpleMovingAverage series by assignin the inner series.
// we need to use a reference because `.Render()` needs to modify state within the series.
smaSeries := &chart.SMASeries{
InnerSeries: mainSeries,
} // we can optionally set the `WindowSize` property which alters how the moving average is calculated.
graph := chart.Chart{
Series: []chart.Series{
mainSeries,
smaSeries,
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,20 +1,22 @@
package main
import (
"os"
"fmt"
"log"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
sbc := chart.StackedBarChart{
Title: "Test Stacked Bar Chart",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
XAxis: chart.Style{
Show: true,
},
YAxis: chart.Style{
Show: true,
},
Bars: []chart.StackedBar{
{
Name: "This is a very long string to test word break wrapping.",
@ -47,7 +49,14 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
sbc.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
err := sbc.Render(chart.PNG, res)
if err != nil {
fmt.Printf("Error rendering chart: %v\n", err)
}
}
func main() {
http.HandleFunc("/", drawChart)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,21 +1,20 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"time"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
xv, yv := xvalues(), yvalues()
priceSeries := chart.TimeSeries{
Name: "SPY",
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0),
},
XValues: xv,
@ -25,6 +24,7 @@ func main() {
smaSeries := chart.SMASeries{
Name: "SPY - SMA",
Style: chart.Style{
Show: true,
StrokeColor: drawing.ColorRed,
StrokeDashArray: []float64{5.0, 5.0},
},
@ -34,6 +34,7 @@ func main() {
bbSeries := &chart.BollingerBandsSeries{
Name: "SPY - Bol. Bands",
Style: chart.Style{
Show: true,
StrokeColor: drawing.ColorFromHex("efefef"),
FillColor: drawing.ColorFromHex("efefef").WithAlpha(64),
},
@ -42,9 +43,11 @@ func main() {
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{Show: true},
TickPosition: chart.TickPositionBetweenTicks,
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
Range: &chart.ContinuousRange{
Max: 220.0,
Min: 180.0,
@ -57,9 +60,8 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func xvalues() []time.Time {
@ -76,3 +78,8 @@ func xvalues() []time.Time {
func yvalues() []float64 {
return []float64{212.47, 212.59, 211.76, 211.37, 210.18, 208.00, 206.79, 209.33, 210.77, 210.82, 210.50, 209.79, 209.38, 210.07, 208.35, 207.95, 210.57, 208.66, 208.92, 208.66, 209.42, 210.59, 209.98, 208.32, 203.97, 197.83, 189.50, 187.27, 194.46, 199.27, 199.28, 197.67, 191.77, 195.41, 195.55, 192.59, 197.43, 194.79, 195.85, 196.74, 196.01, 198.45, 200.18, 199.73, 195.45, 196.46, 193.90, 193.60, 192.90, 192.87, 188.01, 188.12, 191.63, 192.13, 195.00, 198.47, 197.79, 199.41, 201.21, 201.33, 201.52, 200.25, 199.29, 202.35, 203.27, 203.37, 203.11, 201.85, 205.26, 207.51, 207.00, 206.60, 208.95, 208.83, 207.93, 210.39, 211.00, 210.36, 210.15, 210.04, 208.08, 208.56, 207.74, 204.84, 202.54, 205.62, 205.47, 208.73, 208.55, 209.31, 209.07, 209.35, 209.32, 209.56, 208.69, 210.68, 208.53, 205.61, 209.62, 208.35, 206.95, 205.34, 205.87, 201.88, 202.90, 205.03, 208.03, 204.86, 200.02, 201.67, 203.50, 206.02, 205.68, 205.21, 207.40, 205.93, 203.87, 201.02, 201.36, 198.82, 194.05, 191.92, 192.11, 193.66, 188.83, 191.93, 187.81, 188.06, 185.65, 186.69, 190.52, 187.64, 190.20, 188.13, 189.11, 193.72, 193.65, 190.16, 191.30, 191.60, 187.95, 185.42, 185.43, 185.27, 182.86, 186.63, 189.78, 192.88, 192.09, 192.00, 194.78, 192.32, 193.20, 195.54, 195.09, 193.56, 198.11, 199.00, 199.78, 200.43, 200.59, 198.40, 199.38, 199.54, 202.76, 202.50, 202.17, 203.34, 204.63, 204.38, 204.67, 204.56, 203.21, 203.12, 203.24, 205.12, 206.02, 205.52, 206.92, 206.25, 204.19, 206.42, 203.95, 204.50, 204.02, 205.92, 208.00, 208.01, 207.78, 209.24, 209.90, 210.10, 208.97, 208.97, 208.61, 208.92, 209.35, 207.45, 206.33, 207.97, 206.16, 205.01, 204.97, 205.72, 205.89, 208.45, 206.50, 206.56, 204.76, 206.78, 204.85, 204.91, 204.20, 205.49, 205.21, 207.87, 209.28, 209.34, 210.24, 209.84, 210.27, 210.91, 210.28, 211.35, 211.68, 212.37, 212.08, 210.07, 208.45, 208.04, 207.75, 208.37, 206.52, 207.85, 208.44, 208.10, 210.81, 203.24, 199.60, 203.20, 206.66, 209.48, 209.92, 208.41, 209.66, 209.53, 212.65, 213.40, 214.95, 214.92, 216.12, 215.83}
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,15 +1,13 @@
package main
//go:generate go run main.go
import (
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
f, _ := chart.GetDefaultFont()
r, _ := chart.PNG(1024, 1024)
@ -45,7 +43,11 @@ func main() {
StrokeWidth: 2,
})
file, _ := os.Create("output.png")
defer file.Close()
r.Save(file)
res.Header().Set("Content-Type", "image/png")
r.Save(res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

View file

@ -4,15 +4,20 @@ import (
"net/http"
"time"
chart "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
This is an example of using the `TimeSeries` to automatically coerce time.Time values into a continuous xrange.
Note: chart.TimeSeries implements `ValueFormatterProvider` and as a result gives the XAxis the appropriate formatter to use for the ticks.
Note: chart.TimeSeries implements `ValueFormatterProvider` and as a result gives the XAxis the appropariate formatter to use for the ticks.
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.TimeSeries{
XValues: []time.Time{
@ -43,6 +48,9 @@ func drawCustomChart(res http.ResponseWriter, req *http.Request) {
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
ValueFormatter: chart.TimeHourValueFormatter,
},
Series: []chart.Series{
@ -71,7 +79,7 @@ func drawCustomChart(res http.ResponseWriter, req *http.Request) {
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/favicon.ico", func(res http.ResponseWriter, req *http.Request) {
http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) {
res.Write([]byte{})
})
http.HandleFunc("/custom", drawCustomChart)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,15 +1,13 @@
package main
//go:generate go run main.go
import (
"fmt"
"os"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart"
)
func main() {
func drawChart(res http.ResponseWriter, req *http.Request) {
/*
In this example we add a second series, and assign it to the secondary y axis, giving that series it's own range.
@ -19,13 +17,26 @@ func main() {
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{
Show: true, //enables / displays the x-axis
},
TickPosition: chart.TickPositionBetweenTicks,
ValueFormatter: func(v interface{}) string {
typed := v.(float64)
typedDate := chart.TimeFromFloat64(typed)
typedDate := chart.Time.FromFloat64(typed)
return fmt.Sprintf("%d-%d\n%d", typedDate.Month(), typedDate.Day(), typedDate.Year())
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true, //enables / displays the y-axis
},
},
YAxisSecondary: chart.YAxis{
Style: chart.Style{
Show: true, //enables / displays the secondary y-axis
},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
@ -39,7 +50,11 @@ func main() {
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -1,27 +1,32 @@
package main
//go:generate go run main.go
import (
"bytes"
"log"
"os"
"git.smarteching.com/zeni/go-chart/v2"
//"time"
"github.com/wcharczuk/go-chart" //exposes "chart"
)
func main() {
var b float64
b = 1000
ts1 := chart.ContinuousSeries{ //TimeSeries{
Name: "Time Series",
Name: "Time Series",
Style: chart.Style{
Show: true,
},
//XValues: []time.Time{time.Unix(3*b,0),time.Unix(4*b,0),time.Unix(5*b,0),time.Unix(6*b,0),time.Unix(7*b,0),time.Unix(8*b,0),time.Unix(9*b,0),time.Unix(10*b,0)},
XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b},
YValues: []float64{1.0, 2.0, 30.0, 4.0, 50.0, 6.0, 7.0, 88.0},
}
ts2 := chart.ContinuousSeries{ //TimeSeries{
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(1),
},
@ -33,11 +38,15 @@ func main() {
XAxis: chart.XAxis{
Name: "The XAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
ValueFormatter: chart.TimeMinuteValueFormatter, //TimeHourValueFormatter,
},
YAxis: chart.YAxis{
Name: "The YAxis",
Name: "The YAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
Series: []chart.Series{

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -5,11 +5,6 @@ import (
"math"
)
// Interface Assertions.
var (
_ Series = (*AnnotationSeries)(nil)
)
// AnnotationSeries is a series of labels on the chart.
type AnnotationSeries struct {
Name string
@ -53,17 +48,17 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
Right: 0,
Bottom: 0,
}
if !as.Style.Hidden {
if as.Style.IsZero() || as.Style.Show {
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
for _, a := range as.Annotations {
style := a.Style.InheritFrom(seriesStyle)
lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = MinInt(box.Top, ab.Top)
box.Left = MinInt(box.Left, ab.Left)
box.Right = MaxInt(box.Right, ab.Right)
box.Bottom = MaxInt(box.Bottom, ab.Bottom)
box.Top = Math.MinInt(box.Top, ab.Top)
box.Left = Math.MinInt(box.Left, ab.Left)
box.Right = Math.MaxInt(box.Right, ab.Right)
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
}
}
return box
@ -71,7 +66,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
// Render draws the series.
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
if !as.Style.Hidden {
if as.Style.IsZero() || as.Style.Show {
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
for _, a := range as.Annotations {
style := a.Style.InheritFrom(seriesStyle)

View file

@ -4,14 +4,17 @@ import (
"image/color"
"testing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
"github.com/wcharczuk/go-chart/drawing"
)
func TestAnnotationSeriesMeasure(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
as := AnnotationSeries{
Style: Style{
Show: true,
},
Annotations: []Value2{
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
@ -21,10 +24,10 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
}
r, err := PNG(110, 110)
testutil.AssertNil(t, err)
assert.Nil(err)
f, err := GetDefaultFont()
testutil.AssertNil(t, err)
assert.Nil(err)
xrange := &ContinuousRange{
Min: 1.0,
@ -49,18 +52,19 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
}
box := as.Measure(r, cb, xrange, yrange, sd)
testutil.AssertFalse(t, box.IsZero())
testutil.AssertEqual(t, -5.0, box.Top)
testutil.AssertEqual(t, 5.0, box.Left)
testutil.AssertEqual(t, 146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px.
testutil.AssertEqual(t, 115.0, box.Bottom)
assert.False(box.IsZero())
assert.Equal(-5.0, box.Top)
assert.Equal(5.0, box.Left)
assert.Equal(146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px.
assert.Equal(115.0, box.Bottom)
}
func TestAnnotationSeriesRender(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
as := AnnotationSeries{
Style: Style{
Show: true,
FillColor: drawing.ColorWhite,
StrokeColor: drawing.ColorBlack,
},
@ -73,10 +77,10 @@ func TestAnnotationSeriesRender(t *testing.T) {
}
r, err := PNG(110, 110)
testutil.AssertNil(t, err)
assert.Nil(err)
f, err := GetDefaultFont()
testutil.AssertNil(t, err)
assert.Nil(err)
xrange := &ContinuousRange{
Min: 1.0,
@ -103,13 +107,13 @@ func TestAnnotationSeriesRender(t *testing.T) {
as.Render(r, cb, xrange, yrange, sd)
rr, isRaster := r.(*rasterRenderer)
testutil.AssertTrue(t, isRaster)
testutil.AssertNotNil(t, rr)
assert.True(isRaster)
assert.NotNil(rr)
c := rr.i.At(38, 70)
converted, isRGBA := color.RGBAModel.Convert(c).(color.RGBA)
testutil.AssertTrue(t, isRGBA)
testutil.AssertEqual(t, 0, converted.R)
testutil.AssertEqual(t, 0, converted.G)
testutil.AssertEqual(t, 0, converted.B)
assert.True(isRGBA)
assert.Equal(0, converted.R)
assert.Equal(0, converted.G)
assert.Equal(0, converted.B)
}

View file

@ -1,24 +0,0 @@
package chart
var (
_ Sequence = (*Array)(nil)
)
// NewArray returns a new array from a given set of values.
// Array implements Sequence, which allows it to be used with the sequence helpers.
func NewArray(values ...float64) Array {
return Array(values)
}
// Array is a wrapper for an array of floats that implements `ValuesProvider`.
type Array []float64
// Len returns the value provider length.
func (a Array) Len() int {
return len(a)
}
// GetValue returns the value at a given index.
func (a Array) GetValue(index int) float64 {
return a[index]
}

View file

@ -14,8 +14,6 @@ type BarChart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
@ -30,9 +28,6 @@ type BarChart struct {
BarSpacing int
UseBaseValue bool
BaseValue float64
Font *truetype.Font
defaultFont *truetype.Font
@ -128,7 +123,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
yr = bc.setRangeDomains(canvasBox, yr)
}
bc.drawCanvas(r, canvasBox)
bc.drawBars(r, canvasBox, yr)
bc.drawXAxis(r, canvasBox)
bc.drawYAxis(r, canvasBox, yr, yt)
@ -141,10 +136,6 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
return r.Save(w)
}
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, bc.getCanvasStyle())
}
func (bc BarChart) getRanges() Range {
var yrange Range
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
@ -201,20 +192,11 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
by = canvasBox.Bottom - yr.Translate(bar.Value)
if bc.UseBaseValue {
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
}
} else {
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom,
}
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom,
}
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
@ -224,7 +206,7 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
}
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
if !bc.XAxis.Hidden {
if bc.XAxis.Show {
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
axisStyle.WriteToRenderer(r)
@ -263,44 +245,44 @@ func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
}
func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
if !bc.YAxis.Style.Hidden {
bc.YAxis.Render(r, canvasBox, yr, bc.styleDefaultsAxes(), ticks)
if bc.YAxis.Style.Show {
axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes())
axisStyle.WriteToRenderer(r)
r.MoveTo(canvasBox.Right, canvasBox.Top)
r.LineTo(canvasBox.Right, canvasBox.Bottom)
r.Stroke()
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
r.Stroke()
var ty int
var tb Box
for _, t := range ticks {
ty = canvasBox.Bottom - yr.Translate(t.Value)
axisStyle.GetStrokeOptions().WriteToRenderer(r)
r.MoveTo(canvasBox.Right, ty)
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
r.Stroke()
axisStyle.GetTextOptions().WriteToRenderer(r)
tb = r.MeasureText(t.Label)
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
}
}
}
func (bc BarChart) drawTitle(r Renderer) {
if len(bc.Title) > 0 && !bc.TitleStyle.Hidden {
r.SetFont(bc.TitleStyle.GetFont(bc.GetFont()))
r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor()))
titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize())
r.SetFontSize(titleFontSize)
textBox := r.MeasureText(bc.Title)
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (bc.GetWidth() >> 1) - (textWidth >> 1)
titleY := bc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
r.Text(bc.Title, titleX, titleY)
}
}
func (bc BarChart) getCanvasStyle() Style {
return bc.Canvas.InheritFrom(bc.styleDefaultsCanvas())
}
func (bc BarChart) styleDefaultsCanvas() Style {
return Style{
FillColor: bc.GetColorPalette().CanvasColor(),
StrokeColor: bc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultCanvasStrokeWidth,
if len(bc.Title) > 0 && bc.TitleStyle.Show {
Draw.TextWithin(r, bc.Title, bc.box(), bc.styleDefaultsTitle())
}
}
func (bc BarChart) hasAxes() bool {
return !bc.YAxis.Style.Hidden
return bc.YAxis.Style.Show
}
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
@ -320,7 +302,7 @@ func (bc BarChart) getValueFormatters() ValueFormatter {
}
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
if !bc.YAxis.Style.Hidden {
if bc.YAxis.Style.Show {
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
}
return
@ -366,7 +348,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
if !bc.XAxis.Hidden {
if bc.XAxis.Show {
xaxisHeight := DefaultVerticalTickHeight
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
@ -384,7 +366,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
linesBox := Text.MeasureLines(r, lines, axisStyle)
xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
xaxisHeight = Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
}
}
@ -398,7 +380,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
axesOuterBox = axesOuterBox.Grow(xbox)
}
if !bc.YAxis.Style.Hidden {
if bc.YAxis.Style.Show {
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
@ -412,8 +394,8 @@ func (bc BarChart) box() Box {
dpb := bc.Background.Padding.GetBottom(50)
return Box{
Top: bc.Background.Padding.GetTop(20),
Left: bc.Background.Padding.GetLeft(20),
Top: 20,
Left: 20,
Right: bc.GetWidth() - dpr,
Bottom: bc.GetHeight() - dpb,
}
@ -425,23 +407,23 @@ func (bc BarChart) getBackgroundStyle() Style {
func (bc BarChart) styleDefaultsBackground() Style {
return Style{
FillColor: bc.GetColorPalette().BackgroundColor(),
StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
FillColor: DefaultBackgroundColor,
StrokeColor: DefaultBackgroundStrokeColor,
StrokeWidth: DefaultStrokeWidth,
}
}
func (bc BarChart) styleDefaultsBar(index int) Style {
return Style{
StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
StrokeColor: GetAlternateColor(index),
StrokeWidth: 3.0,
FillColor: bc.GetColorPalette().GetSeriesColor(index),
FillColor: GetAlternateColor(index),
}
}
func (bc BarChart) styleDefaultsTitle() Style {
return bc.TitleStyle.InheritFrom(Style{
FontColor: bc.GetColorPalette().TextColor(),
FontColor: DefaultTextColor,
Font: bc.GetFont(),
FontSize: bc.getTitleFontSize(),
TextHorizontalAlign: TextHorizontalAlignCenter,
@ -451,7 +433,7 @@ func (bc BarChart) styleDefaultsTitle() Style {
}
func (bc BarChart) getTitleFontSize() float64 {
effectiveDimension := MinInt(bc.GetWidth(), bc.GetHeight())
effectiveDimension := Math.MinInt(bc.GetWidth(), bc.GetHeight())
if effectiveDimension >= 2048 {
return 48
} else if effectiveDimension >= 1024 {
@ -466,10 +448,10 @@ func (bc BarChart) getTitleFontSize() float64 {
func (bc BarChart) styleDefaultsAxes() Style {
return Style{
StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
StrokeColor: DefaultAxisColor,
Font: bc.GetFont(),
FontSize: DefaultAxisFontSize,
FontColor: bc.GetColorPalette().TextColor(),
FontColor: DefaultAxisColor,
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
@ -481,11 +463,3 @@ func (bc BarChart) styleDefaultsElements() Style {
Font: bc.GetFont(),
}
}
// GetColorPalette returns the color palette for the chart.
func (bc BarChart) GetColorPalette() ColorPalette {
if bc.ColorPalette != nil {
return bc.ColorPalette
}
return AlternateColorPalette
}

View file

@ -5,15 +5,20 @@ import (
"math"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
assert "github.com/blendlabs/go-assert"
)
func TestBarChartRender(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Width: 1024,
Title: "Test Title",
Width: 1024,
Title: "Test Title",
TitleStyle: StyleShow(),
XAxis: StyleShow(),
YAxis: YAxis{
Style: StyleShow(),
},
Bars: []Value{
{Value: 1.0, Label: "One"},
{Value: 2.0, Label: "Two"},
@ -25,16 +30,21 @@ func TestBarChartRender(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf)
testutil.AssertNil(t, err)
testutil.AssertNotZero(t, buf.Len())
assert.Nil(err)
assert.NotZero(buf.Len())
}
func TestBarChartRenderZero(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Width: 1024,
Title: "Test Title",
Width: 1024,
Title: "Test Title",
TitleStyle: StyleShow(),
XAxis: StyleShow(),
YAxis: YAxis{
Style: StyleShow(),
},
Bars: []Value{
{Value: 0.0, Label: "One"},
{Value: 0.0, Label: "Two"},
@ -43,64 +53,64 @@ func TestBarChartRenderZero(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf)
testutil.AssertNotNil(t, err)
assert.NotNil(err)
}
func TestBarChartProps(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
testutil.AssertEqual(t, DefaultDPI, bc.GetDPI())
assert.Equal(DefaultDPI, bc.GetDPI())
bc.DPI = 100
testutil.AssertEqual(t, 100, bc.GetDPI())
assert.Equal(100, bc.GetDPI())
testutil.AssertNil(t, bc.GetFont())
assert.Nil(bc.GetFont())
f, err := GetDefaultFont()
testutil.AssertNil(t, err)
assert.Nil(err)
bc.Font = f
testutil.AssertNotNil(t, bc.GetFont())
assert.NotNil(bc.GetFont())
testutil.AssertEqual(t, DefaultChartWidth, bc.GetWidth())
assert.Equal(DefaultChartWidth, bc.GetWidth())
bc.Width = DefaultChartWidth - 1
testutil.AssertEqual(t, DefaultChartWidth-1, bc.GetWidth())
assert.Equal(DefaultChartWidth-1, bc.GetWidth())
testutil.AssertEqual(t, DefaultChartHeight, bc.GetHeight())
assert.Equal(DefaultChartHeight, bc.GetHeight())
bc.Height = DefaultChartHeight - 1
testutil.AssertEqual(t, DefaultChartHeight-1, bc.GetHeight())
assert.Equal(DefaultChartHeight-1, bc.GetHeight())
testutil.AssertEqual(t, DefaultBarSpacing, bc.GetBarSpacing())
assert.Equal(DefaultBarSpacing, bc.GetBarSpacing())
bc.BarSpacing = 150
testutil.AssertEqual(t, 150, bc.GetBarSpacing())
assert.Equal(150, bc.GetBarSpacing())
testutil.AssertEqual(t, DefaultBarWidth, bc.GetBarWidth())
assert.Equal(DefaultBarWidth, bc.GetBarWidth())
bc.BarWidth = 75
testutil.AssertEqual(t, 75, bc.GetBarWidth())
assert.Equal(75, bc.GetBarWidth())
}
func TestBarChartRenderNoBars(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
testutil.AssertNotNil(t, err)
assert.NotNil(err)
}
func TestBarChartGetRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
assert.NotNil(yr)
assert.False(yr.IsZero())
testutil.AssertEqual(t, -math.MaxFloat64, yr.GetMax())
testutil.AssertEqual(t, math.MaxFloat64, yr.GetMin())
assert.Equal(-math.MaxFloat64, yr.GetMax())
assert.Equal(math.MaxFloat64, yr.GetMin())
}
func TestBarChartGetRangesBarsMinMax(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Bars: []Value{
@ -110,15 +120,15 @@ func TestBarChartGetRangesBarsMinMax(t *testing.T) {
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
assert.NotNil(yr)
assert.False(yr.IsZero())
testutil.AssertEqual(t, 10, yr.GetMax())
testutil.AssertEqual(t, 1, yr.GetMin())
assert.Equal(10, yr.GetMax())
assert.Equal(1, yr.GetMin())
}
func TestBarChartGetRangesMinMax(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
YAxis: YAxis{
@ -138,15 +148,15 @@ func TestBarChartGetRangesMinMax(t *testing.T) {
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
assert.NotNil(yr)
assert.False(yr.IsZero())
testutil.AssertEqual(t, 15, yr.GetMax())
testutil.AssertEqual(t, 5, yr.GetMin())
assert.Equal(15, yr.GetMax())
assert.Equal(5, yr.GetMin())
}
func TestBarChartGetRangesTicksMinMax(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
YAxis: YAxis{
@ -162,56 +172,57 @@ func TestBarChartGetRangesTicksMinMax(t *testing.T) {
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
assert.NotNil(yr)
assert.False(yr.IsZero())
testutil.AssertEqual(t, 11, yr.GetMax())
testutil.AssertEqual(t, 7, yr.GetMin())
assert.Equal(11, yr.GetMax())
assert.Equal(7, yr.GetMin())
}
func TestBarChartHasAxes(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
testutil.AssertTrue(t, bc.hasAxes())
assert.False(bc.hasAxes())
bc.YAxis = YAxis{
Style: Hidden(),
Style: StyleShow(),
}
testutil.AssertFalse(t, bc.hasAxes())
assert.True(bc.hasAxes())
}
func TestBarChartGetDefaultCanvasBox(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
b := bc.getDefaultCanvasBox()
testutil.AssertFalse(t, b.IsZero())
assert.False(b.IsZero())
}
func TestBarChartSetRangeDomains(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
cb := bc.box()
yr := bc.getRanges()
yr2 := bc.setRangeDomains(cb, yr)
testutil.AssertNotZero(t, yr2.GetDomain())
assert.NotZero(yr2.GetDomain())
}
func TestBarChartGetValueFormatters(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{}
vf := bc.getValueFormatters()
testutil.AssertNotNil(t, vf)
testutil.AssertEqual(t, "1234.00", vf(1234.0))
assert.NotNil(vf)
assert.Equal("1234.00", vf(1234.0))
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
testutil.AssertEqual(t, "test", bc.getValueFormatters()(1234))
assert.Equal("test", bc.getValueFormatters()(1234))
}
func TestBarChartGetAxesTicks(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Bars: []Value{
@ -222,21 +233,20 @@ func TestBarChartGetAxesTicks(t *testing.T) {
}
r, err := PNG(128, 128)
testutil.AssertNil(t, err)
assert.Nil(err)
yr := bc.getRanges()
yf := bc.getValueFormatters()
bc.YAxis.Style.Hidden = true
ticks := bc.getAxesTicks(r, yr, yf)
testutil.AssertEmpty(t, ticks)
assert.Empty(ticks)
bc.YAxis.Style.Hidden = false
bc.YAxis.Style.Show = true
ticks = bc.getAxesTicks(r, yr, yf)
testutil.AssertLen(t, ticks, 2)
assert.Len(ticks, 2)
}
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Width: 1024,
@ -251,15 +261,15 @@ func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
}
spacing := bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertNotZero(t, spacing)
assert.NotZero(spacing)
bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertZero(t, spacing)
assert.Zero(spacing)
}
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BarChart{
Width: 1024,
@ -276,35 +286,35 @@ func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
cb := bc.box()
spacing := bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertNotZero(t, spacing)
assert.NotZero(spacing)
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
testutil.AssertEqual(t, 10, barWidth)
assert.Equal(10, barWidth)
bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertZero(t, spacing)
assert.Zero(spacing)
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
testutil.AssertEqual(t, 199, barWidth)
assert.Equal(199, barWidth)
testutil.AssertEqual(t, cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
assert.Equal(cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
bw, bs, total := bc.calculateScaledTotalWidth(cb)
testutil.AssertEqual(t, spacing, bs)
testutil.AssertEqual(t, barWidth, bw)
testutil.AssertEqual(t, cb.Width()+1, total)
assert.Equal(spacing, bs)
assert.Equal(barWidth, bw)
assert.Equal(cb.Width()+1, total)
}
func TestBarChatGetTitleFontSize(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
testutil.AssertEqual(t, 48, size)
assert.Equal(48, size)
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
testutil.AssertEqual(t, 24, size)
assert.Equal(24, size)
size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
testutil.AssertEqual(t, 18, size)
assert.Equal(18, size)
size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
testutil.AssertEqual(t, 12, size)
assert.Equal(12, size)
size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
testutil.AssertEqual(t, 10, size)
assert.Equal(10, size)
}

View file

@ -2,11 +2,7 @@ package chart
import (
"fmt"
)
// Interface Assertions.
var (
_ Series = (*BollingerBandsSeries)(nil)
"math"
)
// BollingerBandsSeries draws bollinger bands for an inner series.
@ -18,9 +14,9 @@ type BollingerBandsSeries struct {
Period int
K float64
InnerSeries ValuesProvider
InnerSeries ValueProvider
valueBuffer *ValueBuffer
valueBuffer *RingBuffer
}
// GetName returns the name of the time series.
@ -46,9 +42,7 @@ func (bbs BollingerBandsSeries) GetPeriod() int {
return bbs.Period
}
// GetK returns the K value, or the number of standard deviations above and below
// to band the simple moving average with.
// Typical K value is 2.0.
// GetK returns the K value.
func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
if bbs.K == 0 {
if len(defaults) > 0 {
@ -60,35 +54,35 @@ func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
}
// Len returns the number of elements in the series.
func (bbs BollingerBandsSeries) Len() int {
func (bbs *BollingerBandsSeries) Len() int {
return bbs.InnerSeries.Len()
}
// GetBoundedValues gets the bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
// GetBoundedValue gets the bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedValue(index int) (x, y1, y2 float64) {
if bbs.InnerSeries == nil {
return
}
if bbs.valueBuffer == nil || index == 0 {
bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
bbs.valueBuffer = NewRingBufferWithCapacity(bbs.GetPeriod())
}
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
bbs.valueBuffer.Dequeue()
}
px, py := bbs.InnerSeries.GetValues(index)
px, py := bbs.InnerSeries.GetValue(index)
bbs.valueBuffer.Enqueue(py)
x = px
ay := Seq{bbs.valueBuffer}.Average()
std := Seq{bbs.valueBuffer}.StdDev()
ay := bbs.getAverage(bbs.valueBuffer)
std := bbs.getStdDev(bbs.valueBuffer)
y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std)
return
}
// GetBoundedLastValues returns the last bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
// GetBoundedLastValue returns the last bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) {
if bbs.InnerSeries == nil {
return
}
@ -99,15 +93,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
startAt = 0
}
vb := NewValueBufferWithCapacity(period)
vb := NewRingBufferWithCapacity(period)
for index := startAt; index < seriesLength; index++ {
xn, yn := bbs.InnerSeries.GetValues(index)
xn, yn := bbs.InnerSeries.GetValue(index)
vb.Enqueue(yn)
x = xn
}
ay := Seq{vb}.Average()
std := Seq{vb}.StdDev()
ay := bbs.getAverage(vb)
std := bbs.getStdDev(vb)
y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std)
@ -126,6 +120,37 @@ func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrang
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
}
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {
var accum float64
valueBuffer.Each(func(v interface{}) {
if typed, isTyped := v.(float64); isTyped {
accum += typed
}
})
return accum / float64(valueBuffer.Len())
}
func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 {
if valueBuffer.Len() == 0 {
return 0
}
var variance float64
m := bbs.getAverage(valueBuffer)
valueBuffer.Each(func(v interface{}) {
if n, isTyped := v.(float64); isTyped {
variance += (float64(n) - m) * (float64(n) - m)
}
})
return variance / float64(valueBuffer.Len())
}
func (bbs BollingerBandsSeries) getStdDev(valueBuffer *RingBuffer) float64 {
return math.Pow(bbs.getVariance(valueBuffer), 0.5)
}
// Validate validates the series.
func (bbs BollingerBandsSeries) Validate() error {
if bbs.InnerSeries == nil {

View file

@ -1,19 +1,18 @@
package chart
import (
"fmt"
"math"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
)
func TestBollingerBandSeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
s1 := mockValuesProvider{
X: LinearRange(1.0, 100.0),
Y: RandomValuesWithMax(100, 1024),
s1 := mockValueProvider{
X: Sequence.Float64(1.0, 100.0),
Y: Sequence.Random(100, 1024),
}
bbs := &BollingerBandsSeries{
@ -25,28 +24,28 @@ func TestBollingerBandSeries(t *testing.T) {
y2values := make([]float64, 100)
for x := 0; x < 100; x++ {
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x)
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValue(x)
}
for x := bbs.GetPeriod(); x < 100; x++ {
testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
assert.True(y1values[x] > y2values[x])
}
}
func TestBollingerBandLastValue(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
s1 := mockValuesProvider{
X: LinearRange(1.0, 100.0),
Y: LinearRange(1.0, 100.0),
s1 := mockValueProvider{
X: Sequence.Float64(1.0, 100.0),
Y: Sequence.Float64(1.0, 100.0),
}
bbs := &BollingerBandsSeries{
InnerSeries: s1,
}
x, y1, y2 := bbs.GetBoundedLastValues()
testutil.AssertEqual(t, 100.0, x)
testutil.AssertEqual(t, 101, math.Floor(y1))
testutil.AssertEqual(t, 83, math.Floor(y2))
x, y1, y2 := bbs.GetBoundedLastValue()
assert.Equal(100.0, x)
assert.Equal(101, math.Floor(y1))
assert.Equal(83, math.Floor(y2))
}

View file

@ -1,36 +0,0 @@
package chart
import "fmt"
// BoundedLastValuesAnnotationSeries returns a last value annotation series for a bounded values provider.
func BoundedLastValuesAnnotationSeries(innerSeries FullBoundedValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
lvx, lvy1, lvy2 := innerSeries.GetBoundedLastValues()
var vf ValueFormatter
if len(vfs) > 0 {
vf = vfs[0]
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
_, vf = typed.GetValueFormatters()
} else {
vf = FloatValueFormatter
}
label1 := vf(lvy1)
label2 := vf(lvy2)
var seriesName string
var seriesStyle Style
if typed, isTyped := innerSeries.(Series); isTyped {
seriesName = fmt.Sprintf("%s - Last Values", typed.GetName())
seriesStyle = typed.GetStyle()
}
return AnnotationSeries{
Name: seriesName,
Style: seriesStyle,
Annotations: []Value2{
{XValue: lvx, YValue: lvy1, Label: label1},
{XValue: lvx, YValue: lvy2, Label: label2},
},
}
}

70
box.go
View file

@ -89,12 +89,12 @@ func (b Box) GetBottom(defaults ...int) int {
// Width returns the width
func (b Box) Width() int {
return AbsInt(b.Right - b.Left)
return Math.AbsInt(b.Right - b.Left)
}
// Height returns the height
func (b Box) Height() int {
return AbsInt(b.Bottom - b.Top)
return Math.AbsInt(b.Bottom - b.Top)
}
// Center returns the center of the box
@ -146,10 +146,10 @@ func (b Box) Equals(other Box) bool {
// Grow grows a box based on another box.
func (b Box) Grow(other Box) Box {
return Box{
Top: MinInt(b.Top, other.Top),
Left: MinInt(b.Left, other.Left),
Right: MaxInt(b.Right, other.Right),
Bottom: MaxInt(b.Bottom, other.Bottom),
Top: Math.MinInt(b.Top, other.Top),
Left: Math.MinInt(b.Left, other.Left),
Right: Math.MaxInt(b.Right, other.Right),
Bottom: Math.MaxInt(b.Bottom, other.Bottom),
}
}
@ -220,10 +220,10 @@ func (b Box) Fit(other Box) Box {
func (b Box) Constrain(other Box) Box {
newBox := b.Clone()
newBox.Top = MaxInt(newBox.Top, other.Top)
newBox.Left = MaxInt(newBox.Left, other.Left)
newBox.Right = MinInt(newBox.Right, other.Right)
newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
newBox.Right = Math.MinInt(newBox.Right, other.Right)
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
return newBox
}
@ -254,22 +254,6 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
return newBox
}
func (b Box) Validate() error {
if b.Left < 0 {
return fmt.Errorf("invalid left; must be >= 0")
}
if b.Right < 0 {
return fmt.Errorf("invalid right; must be > 0")
}
if b.Top < 0 {
return fmt.Errorf("invalid top; must be > 0")
}
if b.Bottom < 0 {
return fmt.Errorf("invalid bottom; must be > 0")
}
return nil
}
// BoxCorners is a box with independent corners.
type BoxCorners struct {
TopLeft, TopRight, BottomRight, BottomLeft Point
@ -278,36 +262,36 @@ type BoxCorners struct {
// Box return the BoxCorners as a regular box.
func (bc BoxCorners) Box() Box {
return Box{
Top: MinInt(bc.TopLeft.Y, bc.TopRight.Y),
Left: MinInt(bc.TopLeft.X, bc.BottomLeft.X),
Right: MaxInt(bc.TopRight.X, bc.BottomRight.X),
Bottom: MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
Top: Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
Left: Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
Right: Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
Bottom: Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
}
}
// Width returns the width
func (bc BoxCorners) Width() int {
minLeft := MinInt(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := MaxInt(bc.TopRight.X, bc.BottomRight.X)
minLeft := Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
return maxRight - minLeft
}
// Height returns the height
func (bc BoxCorners) Height() int {
minTop := MinInt(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
minTop := Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
return maxBottom - minTop
}
// Center returns the center of the box
func (bc BoxCorners) Center() (x, y int) {
left := MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
right := MeanInt(bc.TopRight.X, bc.BottomRight.X)
left := Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
right := Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
x = ((right - left) >> 1) + left
top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
top := Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
bottom := Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
y = ((bottom - top) >> 1) + top
return
@ -317,12 +301,12 @@ func (bc BoxCorners) Center() (x, y int) {
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
cx, cy := bc.Center()
thetaRadians := DegreesToRadians(thetaDegrees)
thetaRadians := Math.DegreesToRadians(thetaDegrees)
tlx, tly := RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
trx, try := RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
brx, bry := RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
blx, bly := RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
tlx, tly := Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
trx, try := Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
brx, bry := Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
blx, bly := Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
return BoxCorners{
TopLeft: Point{tlx, tly},

View file

@ -4,131 +4,131 @@ import (
"math"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
)
func TestBoxClone(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := a.Clone()
testutil.AssertTrue(t, a.Equals(b))
testutil.AssertTrue(t, b.Equals(a))
assert.True(a.Equals(b))
assert.True(b.Equals(a))
}
func TestBoxEquals(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := Box{Top: 10, Left: 10, Right: 30, Bottom: 30}
c := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
testutil.AssertTrue(t, a.Equals(a))
testutil.AssertTrue(t, a.Equals(c))
testutil.AssertTrue(t, c.Equals(a))
testutil.AssertFalse(t, a.Equals(b))
testutil.AssertFalse(t, c.Equals(b))
testutil.AssertFalse(t, b.Equals(a))
testutil.AssertFalse(t, b.Equals(c))
assert.True(a.Equals(a))
assert.True(a.Equals(c))
assert.True(c.Equals(a))
assert.False(a.Equals(b))
assert.False(c.Equals(b))
assert.False(b.Equals(a))
assert.False(b.Equals(c))
}
func TestBoxIsBiggerThan(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
testutil.AssertTrue(t, a.IsBiggerThan(b))
testutil.AssertFalse(t, a.IsBiggerThan(c))
testutil.AssertTrue(t, c.IsBiggerThan(a))
assert.True(a.IsBiggerThan(b))
assert.False(a.IsBiggerThan(c))
assert.True(c.IsBiggerThan(a))
}
func TestBoxIsSmallerThan(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
testutil.AssertFalse(t, a.IsSmallerThan(b))
testutil.AssertTrue(t, a.IsSmallerThan(c))
testutil.AssertFalse(t, c.IsSmallerThan(a))
assert.False(a.IsSmallerThan(b))
assert.True(a.IsSmallerThan(c))
assert.False(c.IsSmallerThan(a))
}
func TestBoxGrow(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 1, Left: 2, Right: 15, Bottom: 15}
b := Box{Top: 4, Left: 5, Right: 30, Bottom: 35}
c := a.Grow(b)
testutil.AssertFalse(t, c.Equals(b))
testutil.AssertFalse(t, c.Equals(a))
testutil.AssertEqual(t, 1, c.Top)
testutil.AssertEqual(t, 2, c.Left)
testutil.AssertEqual(t, 30, c.Right)
testutil.AssertEqual(t, 35, c.Bottom)
assert.False(c.Equals(b))
assert.False(c.Equals(a))
assert.Equal(1, c.Top)
assert.Equal(2, c.Left)
assert.Equal(30, c.Right)
assert.Equal(35, c.Bottom)
}
func TestBoxFit(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
fab := a.Fit(b)
testutil.AssertEqual(t, a.Left, fab.Left)
testutil.AssertEqual(t, a.Right, fab.Right)
testutil.AssertTrue(t, fab.Top < fab.Bottom)
testutil.AssertTrue(t, fab.Left < fab.Right)
testutil.AssertTrue(t, math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
assert.Equal(a.Left, fab.Left)
assert.Equal(a.Right, fab.Right)
assert.True(fab.Top < fab.Bottom)
assert.True(fab.Left < fab.Right)
assert.True(math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
fac := a.Fit(c)
testutil.AssertEqual(t, a.Top, fac.Top)
testutil.AssertEqual(t, a.Bottom, fac.Bottom)
testutil.AssertTrue(t, math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
assert.Equal(a.Top, fac.Top)
assert.Equal(a.Bottom, fac.Bottom)
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
}
func TestBoxConstrain(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
cab := a.Constrain(b)
testutil.AssertEqual(t, 64, cab.Top)
testutil.AssertEqual(t, 64, cab.Left)
testutil.AssertEqual(t, 192, cab.Right)
testutil.AssertEqual(t, 170, cab.Bottom)
assert.Equal(64, cab.Top)
assert.Equal(64, cab.Left)
assert.Equal(192, cab.Right)
assert.Equal(170, cab.Bottom)
cac := a.Constrain(c)
testutil.AssertEqual(t, 64, cac.Top)
testutil.AssertEqual(t, 64, cac.Left)
testutil.AssertEqual(t, 170, cac.Right)
testutil.AssertEqual(t, 192, cac.Bottom)
assert.Equal(64, cac.Top)
assert.Equal(64, cac.Left)
assert.Equal(170, cac.Right)
assert.Equal(192, cac.Bottom)
}
func TestBoxOuterConstrain(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
box := NewBox(0, 0, 100, 100)
canvas := NewBox(5, 5, 95, 95)
taller := NewBox(-10, 5, 50, 50)
c := canvas.OuterConstrain(box, taller)
testutil.AssertEqual(t, 15, c.Top, c.String())
testutil.AssertEqual(t, 5, c.Left, c.String())
testutil.AssertEqual(t, 95, c.Right, c.String())
testutil.AssertEqual(t, 95, c.Bottom, c.String())
assert.Equal(15, c.Top, c.String())
assert.Equal(5, c.Left, c.String())
assert.Equal(95, c.Right, c.String())
assert.Equal(95, c.Bottom, c.String())
wider := NewBox(5, 5, 110, 50)
d := canvas.OuterConstrain(box, wider)
testutil.AssertEqual(t, 5, d.Top, d.String())
testutil.AssertEqual(t, 5, d.Left, d.String())
testutil.AssertEqual(t, 85, d.Right, d.String())
testutil.AssertEqual(t, 95, d.Bottom, d.String())
assert.Equal(5, d.Top, d.String())
assert.Equal(5, d.Left, d.String())
assert.Equal(85, d.Right, d.String())
assert.Equal(95, d.Bottom, d.String())
}
func TestBoxShift(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
b := Box{
Top: 5,
@ -138,14 +138,14 @@ func TestBoxShift(t *testing.T) {
}
shifted := b.Shift(1, 2)
testutil.AssertEqual(t, 7, shifted.Top)
testutil.AssertEqual(t, 6, shifted.Left)
testutil.AssertEqual(t, 11, shifted.Right)
testutil.AssertEqual(t, 12, shifted.Bottom)
assert.Equal(7, shifted.Top)
assert.Equal(6, shifted.Left)
assert.Equal(11, shifted.Right)
assert.Equal(12, shifted.Bottom)
}
func TestBoxCenter(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
b := Box{
Top: 10,
@ -154,12 +154,12 @@ func TestBoxCenter(t *testing.T) {
Bottom: 30,
}
cx, cy := b.Center()
testutil.AssertEqual(t, 15, cx)
testutil.AssertEqual(t, 20, cy)
assert.Equal(15, cx)
assert.Equal(20, cy)
}
func TestBoxCornersCenter(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BoxCorners{
TopLeft: Point{5, 5},
@ -169,12 +169,12 @@ func TestBoxCornersCenter(t *testing.T) {
}
cx, cy := bc.Center()
testutil.AssertEqual(t, 10, cx)
testutil.AssertEqual(t, 10, cy)
assert.Equal(10, cx)
assert.Equal(10, cy)
}
func TestBoxCornersRotate(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
bc := BoxCorners{
TopLeft: Point{5, 5},
@ -184,5 +184,5 @@ func TestBoxCornersRotate(t *testing.T) {
}
rotated := bc.Rotate(45)
testutil.AssertTrue(t, rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
}

128
chart.go
View file

@ -14,8 +14,6 @@ type Chart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
@ -32,8 +30,6 @@ type Chart struct {
Series []Series
Elements []Renderable
Log Logger
}
// GetDPI returns the dpi for the chart.
@ -76,8 +72,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
if len(c.Series) == 0 {
return errors.New("please provide at least one series")
}
if err := c.checkHasVisibleSeries(); err != nil {
return err
if visibleSeriesErr := c.checkHasVisibleSeries(); visibleSeriesErr != nil {
return visibleSeriesErr
}
c.YAxisSecondary.AxisType = YAxisSecondary
@ -102,13 +98,11 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
xr, yr, yra := c.getRanges()
canvasBox := c.getDefaultCanvasBox()
xf, yf, yfa := c.getValueFormatters()
Debugf(c.Log, "chart; canvas box: %v", canvasBox)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
err = c.checkRanges(xr, yr, yra)
if err != nil {
// (try to) dump the raw background to the stream.
r.Save(w)
return err
}
@ -118,8 +112,6 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
Debugf(c.Log, "chart; axes adjusted canvas box: %v", canvasBox)
// do a second pass in case things haven't settled yet.
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
@ -130,8 +122,6 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
Debugf(c.Log, "chart; annotation adjusted canvas box: %v", canvasBox)
}
c.drawCanvas(r, canvasBox)
@ -150,14 +140,16 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
}
func (c Chart) checkHasVisibleSeries() error {
hasVisibleSeries := false
var style Style
for _, s := range c.Series {
style = s.GetStyle()
if !style.Hidden {
return nil
}
hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show)
}
return fmt.Errorf("chart render; must have (1) visible series")
if !hasVisibleSeries {
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
}
return nil
}
func (c Chart) validateSeries() error {
@ -181,12 +173,12 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
// note: a possible future optimization is to not scan the series values if
// all axis are represented by either custom ticks or custom ranges.
for _, s := range c.Series {
if !s.GetStyle().Hidden {
if s.GetStyle().IsZero() || s.GetStyle().Show {
seriesAxis := s.GetYAxis()
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
if bvp, isBoundedValueProvider := s.(BoundedValueProvider); isBoundedValueProvider {
seriesLength := bvp.Len()
for index := 0; index < seriesLength; index++ {
vx, vy1, vy2 := bvp.GetBoundedValues(index)
vx, vy1, vy2 := bvp.GetBoundedValue(index)
minx = math.Min(minx, vx)
maxx = math.Max(maxx, vx)
@ -204,10 +196,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
seriesMappedToSecondaryAxis = true
}
}
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
} else if vp, isValueProvider := s.(ValueProvider); isValueProvider {
seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValues(index)
vx, vy := vp.GetValue(index)
minx = math.Min(minx, vx)
maxx = math.Max(maxx, vx)
@ -268,14 +260,11 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrange.SetMin(miny)
yrange.SetMax(maxy)
if !c.YAxis.Style.Hidden {
delta := yrange.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
yrange.SetMin(rmin)
yrange.SetMax(rmax)
}
delta := yrange.GetDelta()
roundTo := Math.GetRoundToForDelta(delta)
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
yrange.SetMin(rmin)
yrange.SetMax(rmax)
}
if len(c.YAxisSecondary.Ticks) > 0 {
@ -290,20 +279,17 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrangeAlt.SetMin(minya)
yrangeAlt.SetMax(maxya)
if !c.YAxisSecondary.Style.Hidden {
delta := yrangeAlt.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
yrangeAlt.SetMin(rmin)
yrangeAlt.SetMax(rmax)
}
delta := yrangeAlt.GetDelta()
roundTo := Math.GetRoundToForDelta(delta)
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
yrangeAlt.SetMin(rmin)
yrangeAlt.SetMax(rmax)
}
return
}
func (c Chart) checkRanges(xr, yr, yra Range) error {
Debugf(c.Log, "checking xrange: %v", xr)
xDelta := xr.GetDelta()
if math.IsInf(xDelta, 0) {
return errors.New("infinite x-range delta")
@ -315,7 +301,6 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
return errors.New("zero x-range delta; there needs to be at least (2) values")
}
Debugf(c.Log, "checking yrange: %v", yr)
yDelta := yr.GetDelta()
if math.IsInf(yDelta, 0) {
return errors.New("infinite y-range delta")
@ -325,7 +310,6 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
}
if c.hasSecondarySeries() {
Debugf(c.Log, "checking secondary yrange: %v", yra)
yraDelta := yra.GetDelta()
if math.IsInf(yraDelta, 0) {
return errors.New("infinite secondary y-range delta")
@ -356,29 +340,29 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
}
}
if c.XAxis.ValueFormatter != nil {
x = c.XAxis.GetValueFormatter()
x = c.XAxis.ValueFormatter
}
if c.YAxis.ValueFormatter != nil {
y = c.YAxis.GetValueFormatter()
y = c.YAxis.ValueFormatter
}
if c.YAxisSecondary.ValueFormatter != nil {
ya = c.YAxisSecondary.GetValueFormatter()
ya = c.YAxisSecondary.ValueFormatter
}
return
}
func (c Chart) hasAxes() bool {
return !c.XAxis.Style.Hidden || !c.YAxis.Style.Hidden || !c.YAxisSecondary.Style.Hidden
return c.XAxis.Style.Show || c.YAxis.Style.Show || c.YAxisSecondary.Style.Show
}
func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) {
if !c.XAxis.Style.Hidden {
if c.XAxis.Style.Show {
xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf)
}
if !c.YAxis.Style.Hidden {
if c.YAxis.Style.Show {
yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
}
if !c.YAxisSecondary.Style.Hidden {
if c.YAxisSecondary.Style.Show {
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
}
return
@ -386,19 +370,16 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
axesOuterBox := canvasBox.Clone()
if !c.XAxis.Style.Hidden {
if c.XAxis.Style.Show {
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
Debugf(c.Log, "chart; x-axis measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
if !c.YAxis.Style.Hidden {
if c.YAxis.Style.Show {
axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks)
Debugf(c.Log, "chart; y-axis measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
if !c.YAxisSecondary.Style.Hidden && c.hasSecondarySeries() {
if c.YAxisSecondary.Style.Show {
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt)
Debugf(c.Log, "chart; y-axis secondary measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
@ -415,7 +396,7 @@ func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range,
func (c Chart) hasAnnotationSeries() bool {
for _, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if !as.GetStyle().Hidden {
if as.Style.IsZero() || as.Style.Show {
return true
}
}
@ -436,7 +417,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
annotationSeriesBox := canvasBox.Clone()
for seriesIndex, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if !as.GetStyle().Hidden {
if as.Style.IsZero() || as.Style.Show {
style := c.styleDefaultsSeries(seriesIndex)
var annotationBounds Box
if as.YAxis == YAxisPrimary {
@ -473,19 +454,19 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
}
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
if !c.XAxis.Style.Hidden {
if c.XAxis.Style.Show {
c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
}
if !c.YAxis.Style.Hidden {
if c.YAxis.Style.Show {
c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
}
if !c.YAxisSecondary.Style.Hidden {
if c.YAxisSecondary.Style.Show {
c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt)
}
}
func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
if !s.GetStyle().Hidden {
if s.GetStyle().IsZero() || s.GetStyle().Show {
if s.GetYAxis() == YAxisPrimary {
s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
} else if s.GetYAxis() == YAxisSecondary {
@ -495,9 +476,9 @@ func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt R
}
func (c Chart) drawTitle(r Renderer) {
if len(c.Title) > 0 && !c.TitleStyle.Hidden {
if len(c.Title) > 0 && c.TitleStyle.Show {
r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
r.SetFontColor(c.TitleStyle.GetFontColor(DefaultTextColor))
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize)
@ -515,24 +496,25 @@ func (c Chart) drawTitle(r Renderer) {
func (c Chart) styleDefaultsBackground() Style {
return Style{
FillColor: c.GetColorPalette().BackgroundColor(),
StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
FillColor: DefaultBackgroundColor,
StrokeColor: DefaultBackgroundStrokeColor,
StrokeWidth: DefaultBackgroundStrokeWidth,
}
}
func (c Chart) styleDefaultsCanvas() Style {
return Style{
FillColor: c.GetColorPalette().CanvasColor(),
StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
FillColor: DefaultCanvasColor,
StrokeColor: DefaultCanvasStrokeColor,
StrokeWidth: DefaultCanvasStrokeWidth,
}
}
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
strokeColor := GetDefaultColor(seriesIndex)
return Style{
DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
DotColor: strokeColor,
StrokeColor: strokeColor,
StrokeWidth: DefaultSeriesLineWidth,
Font: c.GetFont(),
FontSize: DefaultFontSize,
@ -542,9 +524,9 @@ func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
func (c Chart) styleDefaultsAxes() Style {
return Style{
Font: c.GetFont(),
FontColor: c.GetColorPalette().TextColor(),
FontColor: DefaultAxisColor,
FontSize: DefaultAxisFontSize,
StrokeColor: c.GetColorPalette().AxisStrokeColor(),
StrokeColor: DefaultAxisColor,
StrokeWidth: DefaultAxisLineWidth,
}
}
@ -555,14 +537,6 @@ func (c Chart) styleDefaultsElements() Style {
}
}
// GetColorPalette returns the color palette for the chart.
func (c Chart) GetColorPalette() ColorPalette {
if c.ColorPalette != nil {
return c.ColorPalette
}
return DefaultColorPalette
}
// Box returns the chart bounds as a box.
func (c Chart) Box() Box {
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)

View file

@ -8,57 +8,57 @@ import (
"testing"
"time"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
"github.com/wcharczuk/go-chart/drawing"
)
func TestChartGetDPI(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
unset := Chart{}
testutil.AssertEqual(t, DefaultDPI, unset.GetDPI())
testutil.AssertEqual(t, 192, unset.GetDPI(192))
assert.Equal(DefaultDPI, unset.GetDPI())
assert.Equal(192, unset.GetDPI(192))
set := Chart{DPI: 128}
testutil.AssertEqual(t, 128, set.GetDPI())
testutil.AssertEqual(t, 128, set.GetDPI(192))
assert.Equal(128, set.GetDPI())
assert.Equal(128, set.GetDPI(192))
}
func TestChartGetFont(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
f, err := GetDefaultFont()
testutil.AssertNil(t, err)
assert.Nil(err)
unset := Chart{}
testutil.AssertNil(t, unset.GetFont())
assert.Nil(unset.GetFont())
set := Chart{Font: f}
testutil.AssertNotNil(t, set.GetFont())
assert.NotNil(set.GetFont())
}
func TestChartGetWidth(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
unset := Chart{}
testutil.AssertEqual(t, DefaultChartWidth, unset.GetWidth())
assert.Equal(DefaultChartWidth, unset.GetWidth())
set := Chart{Width: DefaultChartWidth + 10}
testutil.AssertEqual(t, DefaultChartWidth+10, set.GetWidth())
assert.Equal(DefaultChartWidth+10, set.GetWidth())
}
func TestChartGetHeight(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
unset := Chart{}
testutil.AssertEqual(t, DefaultChartHeight, unset.GetHeight())
assert.Equal(DefaultChartHeight, unset.GetHeight())
set := Chart{Height: DefaultChartHeight + 10}
testutil.AssertEqual(t, DefaultChartHeight+10, set.GetHeight())
assert.Equal(DefaultChartHeight+10, set.GetHeight())
}
func TestChartGetRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Series: []Series{
@ -79,14 +79,14 @@ func TestChartGetRanges(t *testing.T) {
}
xrange, yrange, yrangeAlt := c.getRanges()
testutil.AssertEqual(t, -2.0, xrange.GetMin())
testutil.AssertEqual(t, 5.0, xrange.GetMax())
assert.Equal(-2.0, xrange.GetMin())
assert.Equal(5.0, xrange.GetMax())
testutil.AssertEqual(t, -2.1, yrange.GetMin())
testutil.AssertEqual(t, 4.5, yrange.GetMax())
assert.Equal(-2.1, yrange.GetMin())
assert.Equal(4.5, yrange.GetMax())
testutil.AssertEqual(t, 10.0, yrangeAlt.GetMin())
testutil.AssertEqual(t, 14.0, yrangeAlt.GetMax())
assert.Equal(10.0, yrangeAlt.GetMin())
assert.Equal(14.0, yrangeAlt.GetMax())
cSet := Chart{
XAxis: XAxis{
@ -116,18 +116,18 @@ func TestChartGetRanges(t *testing.T) {
}
xr2, yr2, yra2 := cSet.getRanges()
testutil.AssertEqual(t, 9.8, xr2.GetMin())
testutil.AssertEqual(t, 19.8, xr2.GetMax())
assert.Equal(9.8, xr2.GetMin())
assert.Equal(19.8, xr2.GetMax())
testutil.AssertEqual(t, 9.9, yr2.GetMin())
testutil.AssertEqual(t, 19.9, yr2.GetMax())
assert.Equal(9.9, yr2.GetMin())
assert.Equal(19.9, yr2.GetMax())
testutil.AssertEqual(t, 9.7, yra2.GetMin())
testutil.AssertEqual(t, 19.7, yra2.GetMax())
assert.Equal(9.7, yra2.GetMin())
assert.Equal(19.7, yra2.GetMax())
}
func TestChartGetRangesUseTicks(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
// this test asserts that ticks should supercede manual ranges when generating the overall ranges.
@ -155,15 +155,15 @@ func TestChartGetRangesUseTicks(t *testing.T) {
}
xr, yr, yar := c.getRanges()
testutil.AssertEqual(t, -2.0, xr.GetMin())
testutil.AssertEqual(t, 2.0, xr.GetMax())
testutil.AssertEqual(t, 0.0, yr.GetMin())
testutil.AssertEqual(t, 5.0, yr.GetMax())
testutil.AssertTrue(t, yar.IsZero(), yar.String())
assert.Equal(-2.0, xr.GetMin())
assert.Equal(2.0, xr.GetMax())
assert.Equal(0.0, yr.GetMin())
assert.Equal(5.0, yr.GetMax())
assert.True(yar.IsZero(), yar.String())
}
func TestChartGetRangesUseUserRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
YAxis: YAxis{
@ -181,15 +181,15 @@ func TestChartGetRangesUseUserRanges(t *testing.T) {
}
xr, yr, yar := c.getRanges()
testutil.AssertEqual(t, -2.0, xr.GetMin())
testutil.AssertEqual(t, 2.0, xr.GetMax())
testutil.AssertEqual(t, -5.0, yr.GetMin())
testutil.AssertEqual(t, 5.0, yr.GetMax())
testutil.AssertTrue(t, yar.IsZero(), yar.String())
assert.Equal(-2.0, xr.GetMin())
assert.Equal(2.0, xr.GetMax())
assert.Equal(-5.0, yr.GetMin())
assert.Equal(5.0, yr.GetMax())
assert.True(yar.IsZero(), yar.String())
}
func TestChartGetBackgroundStyle(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Background: Style{
@ -198,11 +198,11 @@ func TestChartGetBackgroundStyle(t *testing.T) {
}
bs := c.getBackgroundStyle()
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
}
func TestChartGetCanvasStyle(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Canvas: Style{
@ -211,19 +211,19 @@ func TestChartGetCanvasStyle(t *testing.T) {
}
bs := c.getCanvasStyle()
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
}
func TestChartGetDefaultCanvasBox(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{}
canvasBoxDefault := c.getDefaultCanvasBox()
testutil.AssertFalse(t, canvasBoxDefault.IsZero())
testutil.AssertEqual(t, DefaultBackgroundPadding.Top, canvasBoxDefault.Top)
testutil.AssertEqual(t, DefaultBackgroundPadding.Left, canvasBoxDefault.Left)
testutil.AssertEqual(t, c.GetWidth()-DefaultBackgroundPadding.Right, canvasBoxDefault.Right)
testutil.AssertEqual(t, c.GetHeight()-DefaultBackgroundPadding.Bottom, canvasBoxDefault.Bottom)
assert.False(canvasBoxDefault.IsZero())
assert.Equal(DefaultBackgroundPadding.Top, canvasBoxDefault.Top)
assert.Equal(DefaultBackgroundPadding.Left, canvasBoxDefault.Left)
assert.Equal(c.GetWidth()-DefaultBackgroundPadding.Right, canvasBoxDefault.Right)
assert.Equal(c.GetHeight()-DefaultBackgroundPadding.Bottom, canvasBoxDefault.Bottom)
custom := Chart{
Background: Style{
@ -236,15 +236,15 @@ func TestChartGetDefaultCanvasBox(t *testing.T) {
},
}
canvasBoxCustom := custom.getDefaultCanvasBox()
testutil.AssertFalse(t, canvasBoxCustom.IsZero())
testutil.AssertEqual(t, DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top)
testutil.AssertEqual(t, DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left)
testutil.AssertEqual(t, c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right)
testutil.AssertEqual(t, c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom)
assert.False(canvasBoxCustom.IsZero())
assert.Equal(DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top)
assert.Equal(DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left)
assert.Equal(c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right)
assert.Equal(c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom)
}
func TestChartGetValueFormatters(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Series: []Series{
@ -265,95 +265,90 @@ func TestChartGetValueFormatters(t *testing.T) {
}
dxf, dyf, dyaf := c.getValueFormatters()
testutil.AssertNotNil(t, dxf)
testutil.AssertNotNil(t, dyf)
testutil.AssertNotNil(t, dyaf)
assert.NotNil(dxf)
assert.NotNil(dyf)
assert.NotNil(dyaf)
}
func TestChartHasAxes(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
testutil.AssertTrue(t, Chart{}.hasAxes())
testutil.AssertFalse(t, Chart{XAxis: XAxis{Style: Hidden()}, YAxis: YAxis{Style: Hidden()}, YAxisSecondary: YAxis{Style: Hidden()}}.hasAxes())
assert.False(Chart{}.hasAxes())
x := Chart{
XAxis: XAxis{
Style: Hidden(),
},
YAxis: YAxis{
Style: Shown(),
},
YAxisSecondary: YAxis{
Style: Hidden(),
Style: Style{
Show: true,
},
},
}
testutil.AssertTrue(t, x.hasAxes())
assert.True(x.hasAxes())
y := Chart{
XAxis: XAxis{
Style: Shown(),
},
YAxis: YAxis{
Style: Hidden(),
},
YAxisSecondary: YAxis{
Style: Hidden(),
Style: Style{
Show: true,
},
},
}
testutil.AssertTrue(t, y.hasAxes())
assert.True(y.hasAxes())
ya := Chart{
XAxis: XAxis{
Style: Hidden(),
},
YAxis: YAxis{
Style: Hidden(),
},
YAxisSecondary: YAxis{
Style: Shown(),
Style: Style{
Show: true,
},
},
}
testutil.AssertTrue(t, ya.hasAxes())
assert.True(ya.hasAxes())
}
func TestChartGetAxesTicks(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
r, err := PNG(1024, 1024)
testutil.AssertNil(t, err)
assert.Nil(err)
c := Chart{
XAxis: XAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
},
YAxis: YAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
},
YAxisSecondary: YAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
},
}
xr, yr, yar := c.getRanges()
xt, yt, yat := c.getAxesTicks(r, xr, yr, yar, FloatValueFormatter, FloatValueFormatter, FloatValueFormatter)
testutil.AssertNotEmpty(t, xt)
testutil.AssertNotEmpty(t, yt)
testutil.AssertNotEmpty(t, yat)
assert.NotEmpty(xt)
assert.NotEmpty(yt)
assert.NotEmpty(yat)
}
func TestChartSingleSeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
now := time.Now()
c := Chart{
Title: "Hello!",
Width: 1024,
Height: 400,
Title: "Hello!",
TitleStyle: StyleShow(),
Width: 1024,
Height: 400,
YAxis: YAxis{
Style: StyleShow(),
Range: &ContinuousRange{
Min: 0.0,
Max: 4.0,
},
},
XAxis: XAxis{
Style: StyleShow(),
},
Series: []Series{
TimeSeries{
Name: "goog",
@ -365,11 +360,11 @@ func TestChartSingleSeries(t *testing.T) {
buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer)
testutil.AssertNotEmpty(t, buffer.Bytes())
assert.NotEmpty(buffer.Bytes())
}
func TestChartRegressionBadRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Series: []Series{
@ -381,11 +376,11 @@ func TestChartRegressionBadRanges(t *testing.T) {
}
buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer)
testutil.AssertTrue(t, true, "Render needs to finish.")
assert.True(true, "Render needs to finish.")
}
func TestChartRegressionBadRangesByUser(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
YAxis: YAxis{
@ -396,43 +391,43 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
},
Series: []Series{
ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
},
},
}
buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer)
testutil.AssertTrue(t, true, "Render needs to finish.")
assert.True(true, "Render needs to finish.")
}
func TestChartValidatesSeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Series: []Series{
ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
},
},
}
testutil.AssertNil(t, c.validateSeries())
assert.Nil(c.validateSeries())
c = Chart{
Series: []Series{
ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
},
},
}
testutil.AssertNotNil(t, c.validateSeries())
assert.NotNil(c.validateSeries())
}
func TestChartCheckRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
Series: []Series{
@ -444,11 +439,27 @@ func TestChartCheckRanges(t *testing.T) {
}
xr, yr, yra := c.getRanges()
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
assert.Nil(c.checkRanges(xr, yr, yra))
}
func TestChartCheckRangesFailure(t *testing.T) {
assert := assert.New(t)
c := Chart{
Series: []Series{
ContinuousSeries{
XValues: []float64{1.0, 2.0},
YValues: []float64{3.14, 3.14},
},
},
}
xr, yr, yra := c.getRanges()
assert.NotNil(c.checkRanges(xr, yr, yra))
}
func TestChartCheckRangesWithRanges(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
c := Chart{
XAxis: XAxis{
@ -472,7 +483,7 @@ func TestChartCheckRangesWithRanges(t *testing.T) {
}
xr, yr, yra := c.getRanges()
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
assert.Nil(c.checkRanges(xr, yr, yra))
}
func at(i image.Image, x, y int) drawing.Color {
@ -480,115 +491,85 @@ func at(i image.Image, x, y int) drawing.Color {
}
func TestChartE2ELine(t *testing.T) {
// replaced new assertions helper
c := Chart{
Height: 50,
Width: 50,
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Canvas: Style{
Padding: BoxZero,
},
Background: Style{
Padding: BoxZero,
},
Series: []Series{
ContinuousSeries{
XValues: LinearRangeWithStep(0, 4, 1),
YValues: LinearRangeWithStep(0, 4, 1),
},
},
}
var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer)
testutil.AssertNil(t, err)
// do color tests ...
i, err := png.Decode(buffer)
testutil.AssertNil(t, err)
// test the bottom and top of the line
testutil.AssertEqual(t, drawing.ColorWhite, at(i, 0, 0))
testutil.AssertEqual(t, drawing.ColorWhite, at(i, 49, 49))
// test a line mid point
defaultSeriesColor := GetDefaultColor(0)
testutil.AssertEqual(t, defaultSeriesColor, at(i, 0, 49))
testutil.AssertEqual(t, defaultSeriesColor, at(i, 49, 0))
testutil.AssertEqual(t, drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
}
func TestChartE2ELineWithFill(t *testing.T) {
// replaced new assertions helper
logBuffer := new(bytes.Buffer)
assert := assert.New(t)
c := Chart{
Height: 50,
Width: 50,
Canvas: Style{
Padding: BoxZero,
Padding: Box{IsSet: true},
},
Background: Style{
Padding: BoxZero,
Padding: Box{IsSet: true},
},
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Series: []Series{
ContinuousSeries{
Style: Style{
StrokeColor: drawing.ColorBlue,
FillColor: drawing.ColorRed,
},
XValues: LinearRangeWithStep(0, 4, 1),
YValues: LinearRangeWithStep(0, 4, 1),
XValues: Sequence.Float64(0, 4, 1),
YValues: Sequence.Float64(0, 4, 1),
},
},
Log: NewLogger(OptLoggerStdout(logBuffer), OptLoggerStderr(logBuffer)),
}
testutil.AssertEqual(t, 5, len(c.Series[0].(ContinuousSeries).XValues))
testutil.AssertEqual(t, 5, len(c.Series[0].(ContinuousSeries).YValues))
var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer)
testutil.AssertNil(t, err)
assert.Nil(err)
// do color tests ...
i, err := png.Decode(buffer)
testutil.AssertNil(t, err)
assert.Nil(err)
// test the bottom and top of the line
testutil.AssertEqual(t, drawing.ColorWhite, at(i, 0, 0))
testutil.AssertEqual(t, drawing.ColorRed, at(i, 49, 49))
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
assert.Equal(drawing.ColorWhite, at(i, 49, 49))
// test a line mid point
defaultSeriesColor := drawing.ColorBlue
testutil.AssertEqual(t, defaultSeriesColor, at(i, 0, 49))
testutil.AssertEqual(t, defaultSeriesColor, at(i, 49, 0))
defaultSeriesColor := GetDefaultColor(0)
assert.Equal(defaultSeriesColor, at(i, 0, 49))
assert.Equal(defaultSeriesColor, at(i, 49, 0))
assert.Equal(drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
}
func Test_Chart_cve(t *testing.T) {
poc := StackedBarChart{
Title: "poc",
Bars: []StackedBar{
{
Name: "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
Values: []Value{
{Value: 1, Label: "infinite"},
{Value: 1, Label: "loop"},
func TestChartE2ELineWithFill(t *testing.T) {
assert := assert.New(t)
c := Chart{
Height: 50,
Width: 50,
Canvas: Style{
Padding: Box{IsSet: true},
},
Background: Style{
Padding: Box{IsSet: true},
},
Series: []Series{
ContinuousSeries{
Style: Style{
Show: true,
StrokeColor: drawing.ColorBlue,
FillColor: drawing.ColorRed,
},
XValues: Sequence.Float64(0, 4, 1),
YValues: Sequence.Float64(0, 4, 1),
},
},
}
var imgContent bytes.Buffer
err := poc.Render(PNG, &imgContent)
testutil.AssertNotNil(t, err)
var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer)
assert.Nil(err)
// do color tests ...
i, err := png.Decode(buffer)
assert.Nil(err)
// test the bottom and top of the line
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
assert.Equal(drawing.ColorRed, at(i, 49, 49))
// test a line mid point
defaultSeriesColor := drawing.ColorBlue
assert.Equal(defaultSeriesColor, at(i, 0, 49))
assert.Equal(defaultSeriesColor, at(i, 49, 0))
}

View file

@ -1,148 +0,0 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"git.smarteching.com/zeni/go-chart/v2"
)
var (
outputPath = flag.String("output", "", "The output file")
inputFormat = flag.String("format", "csv", "The input format, either 'csv' or 'tsv' (defaults to 'csv')")
inputPath = flag.String("f", "", "The input file")
reverse = flag.Bool("reverse", false, "If we should reverse the inputs")
hideLegend = flag.Bool("hide-legend", false, "If we should omit the chart legend")
hideSMA = flag.Bool("hide-sma", false, "If we should omit simple moving average")
hideLinreg = flag.Bool("hide-linreg", false, "If we should omit linear regressions")
hideLastValues = flag.Bool("hide-last-values", false, "If we should omit last values")
)
func main() {
flag.Parse()
log := chart.NewLogger()
var rawData []byte
var err error
if *inputPath != "" {
if *inputPath == "-" {
rawData, err = ioutil.ReadAll(os.Stdin)
if err != nil {
log.FatalErr(err)
}
} else {
rawData, err = ioutil.ReadFile(*inputPath)
if err != nil {
log.FatalErr(err)
}
}
} else if len(flag.Args()) > 0 {
rawData = []byte(flag.Args()[0])
} else {
flag.Usage()
os.Exit(1)
}
var parts []string
switch *inputFormat {
case "csv":
parts = chart.SplitCSV(string(rawData))
case "tsv":
parts = strings.Split(string(rawData), "\t")
default:
log.FatalErr(fmt.Errorf("invalid format; must be 'csv' or 'tsv'"))
}
yvalues, err := chart.ParseFloats(parts...)
if err != nil {
log.FatalErr(err)
}
if *reverse {
yvalues = chart.ValueSequence(yvalues...).Reverse().Values()
}
var series []chart.Series
mainSeries := chart.ContinuousSeries{
Name: "Values",
XValues: chart.LinearRange(1, float64(len(yvalues))),
YValues: yvalues,
}
series = append(series, mainSeries)
smaSeries := &chart.SMASeries{
Name: "SMA",
Style: chart.Style{
Hidden: *hideSMA,
StrokeColor: chart.ColorRed,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
series = append(series, smaSeries)
linRegSeries := &chart.LinearRegressionSeries{
Name: "Values - Lin. Reg.",
Style: chart.Style{
Hidden: *hideLinreg,
},
InnerSeries: mainSeries,
}
series = append(series, linRegSeries)
mainLastValue := chart.LastValueAnnotationSeries(mainSeries)
mainLastValue.Style = chart.Style{
Hidden: *hideLastValues,
}
series = append(series, mainLastValue)
linregLastValue := chart.LastValueAnnotationSeries(linRegSeries)
linregLastValue.Style = chart.Style{
Hidden: (*hideLastValues || *hideLinreg),
}
series = append(series, linregLastValue)
smaLastValue := chart.LastValueAnnotationSeries(smaSeries)
smaLastValue.Style = chart.Style{
Hidden: (*hideLastValues || *hideSMA),
}
series = append(series, smaLastValue)
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 50,
},
},
Series: series,
}
if !*hideLegend {
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
}
var output *os.File
if *outputPath != "" {
output, err = os.Create(*outputPath)
if err != nil {
log.FatalErr(err)
}
} else {
output, err = ioutil.TempFile("", "*.png")
if err != nil {
log.FatalErr(err)
}
}
if err := graph.Render(chart.PNG, output); err != nil {
log.FatalErr(err)
}
fmt.Fprintln(os.Stdout, output.Name())
os.Exit(0)
}

184
colors.go
View file

@ -1,184 +0,0 @@
package chart
import "git.smarteching.com/zeni/go-chart/v2/drawing"
var (
// ColorWhite is white.
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// ColorBlue is the basic theme blue color.
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
// ColorCyan is the basic theme cyan color.
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
// ColorGreen is the basic theme green color.
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
// ColorRed is the basic theme red color.
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
// ColorOrange is the basic theme orange color.
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
// ColorYellow is the basic theme yellow color.
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
// ColorBlack is the basic theme black color.
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
// ColorLightGray is the basic theme light gray color.
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
// ColorAlternateBlue is a alternate theme color.
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
// ColorAlternateGreen is a alternate theme color.
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
// ColorAlternateGray is a alternate theme color.
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
// ColorAlternateYellow is a alternate theme color.
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
// ColorAlternateLightGray is a alternate theme color.
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
// ColorTransparent is a transparent (alpha zero) color.
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
)
var (
// DefaultBackgroundColor is the default chart background color.
// It is equivalent to css color:white.
DefaultBackgroundColor = ColorWhite
// DefaultBackgroundStrokeColor is the default chart border color.
// It is equivalent to color:white.
DefaultBackgroundStrokeColor = ColorWhite
// DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white.
DefaultCanvasColor = ColorWhite
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
// It is equivalent to css color:white.
DefaultCanvasStrokeColor = ColorWhite
// DefaultTextColor is the default chart text color.
// It is equivalent to #333333.
DefaultTextColor = ColorBlack
// DefaultAxisColor is the default chart axis line color.
// It is equivalent to #333333.
DefaultAxisColor = ColorBlack
// DefaultStrokeColor is the default chart border color.
// It is equivalent to #efefef.
DefaultStrokeColor = ColorLightGray
// DefaultFillColor is the default fill color.
// It is equivalent to #0074d9.
DefaultFillColor = ColorBlue
// DefaultAnnotationFillColor is the default annotation background color.
DefaultAnnotationFillColor = ColorWhite
// DefaultGridLineColor is the default grid line color.
DefaultGridLineColor = ColorLightGray
)
var (
// DefaultColors are a couple default series colors.
DefaultColors = []drawing.Color{
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
// DefaultAlternateColors are a couple alternate colors.
DefaultAlternateColors = []drawing.Color{
ColorAlternateBlue,
ColorAlternateGreen,
ColorAlternateGray,
ColorAlternateYellow,
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
)
// GetDefaultColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetDefaultColor(index int) drawing.Color {
finalIndex := index % len(DefaultColors)
return DefaultColors[finalIndex]
}
// GetAlternateColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetAlternateColor(index int) drawing.Color {
finalIndex := index % len(DefaultAlternateColors)
return DefaultAlternateColors[finalIndex]
}
// ColorPalette is a set of colors that.
type ColorPalette interface {
BackgroundColor() drawing.Color
BackgroundStrokeColor() drawing.Color
CanvasColor() drawing.Color
CanvasStrokeColor() drawing.Color
AxisStrokeColor() drawing.Color
TextColor() drawing.Color
GetSeriesColor(index int) drawing.Color
}
// DefaultColorPalette represents the default palatte.
var DefaultColorPalette defaultColorPalette
type defaultColorPalette struct{}
func (dp defaultColorPalette) BackgroundColor() drawing.Color {
return DefaultBackgroundColor
}
func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
return DefaultBackgroundStrokeColor
}
func (dp defaultColorPalette) CanvasColor() drawing.Color {
return DefaultCanvasColor
}
func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
return DefaultCanvasStrokeColor
}
func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
return DefaultAxisColor
}
func (dp defaultColorPalette) TextColor() drawing.Color {
return DefaultTextColor
}
func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
return GetDefaultColor(index)
}
// AlternateColorPalette represents the default palatte.
var AlternateColorPalette alternateColorPalette
type alternateColorPalette struct{}
func (ap alternateColorPalette) BackgroundColor() drawing.Color {
return DefaultBackgroundColor
}
func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
return DefaultBackgroundStrokeColor
}
func (ap alternateColorPalette) CanvasColor() drawing.Color {
return DefaultCanvasColor
}
func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
return DefaultCanvasStrokeColor
}
func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
return DefaultAxisColor
}
func (ap alternateColorPalette) TextColor() drawing.Color {
return DefaultTextColor
}
func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
return GetAlternateColor(index)
}

View file

@ -7,7 +7,7 @@ type ConcatSeries []Series
func (cs ConcatSeries) Len() int {
total := 0
for _, s := range cs {
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
total += typed.Len()
}
}
@ -19,10 +19,10 @@ func (cs ConcatSeries) Len() int {
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
cursor := 0
for _, s := range cs {
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
len := typed.Len()
if index < cursor+len {
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
x, y = typed.GetValue(index - cursor) //FENCEPOSTS.
return
}
cursor += typed.Len()

View file

@ -3,39 +3,39 @@ package chart
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
assert "github.com/blendlabs/go-assert"
)
func TestConcatSeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
s1 := ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
s2 := ContinuousSeries{
XValues: LinearRange(11, 20.0),
YValues: LinearRange(10.0, 1.0),
XValues: Sequence.Float64(11, 20.0),
YValues: Sequence.Float64(10.0, 1.0),
}
s3 := ContinuousSeries{
XValues: LinearRange(21, 30.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(21, 30.0),
YValues: Sequence.Float64(1.0, 10.0),
}
cs := ConcatSeries([]Series{s1, s2, s3})
testutil.AssertEqual(t, 30, cs.Len())
assert.Equal(30, cs.Len())
x0, y0 := cs.GetValue(0)
testutil.AssertEqual(t, 1.0, x0)
testutil.AssertEqual(t, 1.0, y0)
assert.Equal(1.0, x0)
assert.Equal(1.0, y0)
xm, ym := cs.GetValue(19)
testutil.AssertEqual(t, 20.0, xm)
testutil.AssertEqual(t, 1.0, ym)
assert.Equal(20.0, xm)
assert.Equal(1.0, ym)
xn, yn := cs.GetValue(29)
testutil.AssertEqual(t, 30.0, xn)
testutil.AssertEqual(t, 10.0, yn)
assert.Equal(30.0, xn)
assert.Equal(10.0, yn)
}

View file

@ -62,9 +62,6 @@ func (r *ContinuousRange) SetDomain(domain int) {
// String returns a simple string for the ContinuousRange.
func (r ContinuousRange) String() string {
if r.GetDelta() == 0 {
return "ContinuousRange [empty]"
}
return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
}

View file

@ -3,20 +3,20 @@ package chart
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
)
func TestRangeTranslate(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
r := ContinuousRange{Domain: 1000}
r.Min, r.Max = MinMax(values...)
r.Min, r.Max = Math.MinAndMax(values...)
// delta = ~7.0
// value = ~5.0
// domain = ~1000
// 5/8 * 1000 ~=
testutil.AssertEqual(t, 0, r.Translate(1.0))
testutil.AssertEqual(t, 1000, r.Translate(8.0))
testutil.AssertEqual(t, 572, r.Translate(5.0))
assert.Equal(0, r.Translate(1.0))
assert.Equal(1000, r.Translate(8.0))
assert.Equal(572, r.Translate(5.0))
}

View file

@ -2,13 +2,6 @@ package chart
import "fmt"
// Interface Assertions.
var (
_ Series = (*ContinuousSeries)(nil)
_ FirstValuesProvider = (*ContinuousSeries)(nil)
_ LastValuesProvider = (*ContinuousSeries)(nil)
)
// ContinuousSeries represents a line on a chart.
type ContinuousSeries struct {
Name string
@ -38,18 +31,13 @@ func (cs ContinuousSeries) Len() int {
return len(cs.XValues)
}
// GetValues gets the x,y values at a given index.
func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
// GetValue gets a value at a given index.
func (cs ContinuousSeries) GetValue(index int) (float64, float64) {
return cs.XValues[index], cs.YValues[index]
}
// GetFirstValues gets the first x,y values.
func (cs ContinuousSeries) GetFirstValues() (float64, float64) {
return cs.XValues[0], cs.YValues[0]
}
// GetLastValues gets the last x,y values.
func (cs ContinuousSeries) GetLastValues() (float64, float64) {
// GetLastValue gets the last value.
func (cs ContinuousSeries) GetLastValue() (float64, float64) {
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
}
@ -82,15 +70,11 @@ func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang
// Validate validates the series.
func (cs ContinuousSeries) Validate() error {
if len(cs.XValues) == 0 {
return fmt.Errorf("continuous series; must have xvalues set")
return fmt.Errorf("continuous series must have xvalues set")
}
if len(cs.YValues) == 0 {
return fmt.Errorf("continuous series; must have yvalues set")
}
if len(cs.XValues) != len(cs.YValues) {
return fmt.Errorf("continuous series; must have same length xvalues as yvalues")
return fmt.Errorf("continuous series must have yvalues set")
}
return nil
}

View file

@ -4,35 +4,35 @@ import (
"fmt"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
assert "github.com/blendlabs/go-assert"
)
func TestContinuousSeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
cs := ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
testutil.AssertEqual(t, "Test Series", cs.GetName())
testutil.AssertEqual(t, 10, cs.Len())
x0, y0 := cs.GetValues(0)
testutil.AssertEqual(t, 1.0, x0)
testutil.AssertEqual(t, 1.0, y0)
assert.Equal("Test Series", cs.GetName())
assert.Equal(10, cs.Len())
x0, y0 := cs.GetValue(0)
assert.Equal(1.0, x0)
assert.Equal(1.0, y0)
xn, yn := cs.GetValues(9)
testutil.AssertEqual(t, 10.0, xn)
testutil.AssertEqual(t, 10.0, yn)
xn, yn := cs.GetValue(9)
assert.Equal(10.0, xn)
assert.Equal(10.0, yn)
xn, yn = cs.GetLastValues()
testutil.AssertEqual(t, 10.0, xn)
testutil.AssertEqual(t, 10.0, yn)
xn, yn = cs.GetLastValue()
assert.Equal(10.0, xn)
assert.Equal(10.0, yn)
}
func TestContinuousSeriesValueFormatter(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
cs := ContinuousSeries{
XValueFormatter: func(v interface{}) string {
@ -44,29 +44,29 @@ func TestContinuousSeriesValueFormatter(t *testing.T) {
}
xf, yf := cs.GetValueFormatters()
testutil.AssertEqual(t, "0.100000 foo", xf(0.1))
testutil.AssertEqual(t, "0.100000 bar", yf(0.1))
assert.Equal("0.100000 foo", xf(0.1))
assert.Equal("0.100000 bar", yf(0.1))
}
func TestContinuousSeriesValidate(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
cs := ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
testutil.AssertNil(t, cs.Validate())
assert.Nil(cs.Validate())
cs = ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
}
testutil.AssertNotNil(t, cs.Validate())
assert.NotNil(cs.Validate())
cs = ContinuousSeries{
Name: "Test Series",
YValues: LinearRange(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
testutil.AssertNotNil(t, cs.Validate())
assert.NotNil(cs.Validate())
}

426
date.go Normal file
View file

@ -0,0 +1,426 @@
package chart
import (
"sync"
"time"
)
const (
// AllDaysMask is a bitmask of all the days of the week.
AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
// WeekDaysMask is a bitmask of all the weekdays of the week.
WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
//WeekendDaysMask is a bitmask of the weekend days of the week.
WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
)
var (
// DaysOfWeek are all the time.Weekday in an array for utility purposes.
DaysOfWeek = []time.Weekday{
time.Sunday,
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
time.Saturday,
}
// WeekDays are the business time.Weekday in an array.
WeekDays = []time.Weekday{
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
}
// WeekendDays are the weekend time.Weekday in an array.
WeekendDays = []time.Weekday{
time.Sunday,
time.Saturday,
}
//Epoch is unix epoc saved for utility purposes.
Epoch = time.Unix(0, 0)
)
var (
_easternLock sync.Mutex
_eastern *time.Location
)
// NYSEOpen is when the NYSE opens.
func NYSEOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
// NYSEClose is when the NYSE closes.
func NYSEClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
// NASDAQOpen is when NASDAQ opens.
func NASDAQOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
// NASDAQClose is when NASDAQ closes.
func NASDAQClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
// NYSEArcaOpen is when NYSEARCA opens.
func NYSEArcaOpen() time.Time { return Date.Time(4, 0, 0, 0, Date.Eastern()) }
// NYSEArcaClose is when NYSEARCA closes.
func NYSEArcaClose() time.Time { return Date.Time(20, 0, 0, 0, Date.Eastern()) }
// HolidayProvider is a function that returns if a given time falls on a holiday.
type HolidayProvider func(time.Time) bool
// defaultHolidayProvider implements `HolidayProvider` and just returns false.
func defaultHolidayProvider(_ time.Time) bool { return false }
var (
// Date contains utility functions that operate on dates.
Date = &date{}
)
type date struct{}
// IsNYSEHoliday returns if a date was/is on a nyse holiday day.
func (d date) IsNYSEHoliday(t time.Time) bool {
te := t.In(d.Eastern())
if te.Year() == 2013 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 21
} else if te.Month() == 2 {
return te.Day() == 18
} else if te.Month() == 3 {
return te.Day() == 29
} else if te.Month() == 5 {
return te.Day() == 27
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 2
} else if te.Month() == 11 {
return te.Day() == 28
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2014 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 20
} else if te.Month() == 2 {
return te.Day() == 17
} else if te.Month() == 4 {
return te.Day() == 18
} else if te.Month() == 5 {
return te.Day() == 26
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 1
} else if te.Month() == 11 {
return te.Day() == 27
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2015 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 19
} else if te.Month() == 2 {
return te.Day() == 16
} else if te.Month() == 4 {
return te.Day() == 3
} else if te.Month() == 5 {
return te.Day() == 25
} else if te.Month() == 7 {
return te.Day() == 3
} else if te.Month() == 9 {
return te.Day() == 7
} else if te.Month() == 11 {
return te.Day() == 26
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2016 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 18
} else if te.Month() == 2 {
return te.Day() == 15
} else if te.Month() == 3 {
return te.Day() == 25
} else if te.Month() == 5 {
return te.Day() == 30
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 5
} else if te.Month() == 11 {
return te.Day() == 24 || te.Day() == 25
} else if te.Month() == 12 {
return te.Day() == 26
}
} else if te.Year() == 2017 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 16
} else if te.Month() == 2 {
return te.Day() == 20
} else if te.Month() == 4 {
return te.Day() == 15
} else if te.Month() == 5 {
return te.Day() == 29
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 4
} else if te.Month() == 11 {
return te.Day() == 23
} else if te.Month() == 12 {
return te.Day() == 25
}
} else if te.Year() == 2018 {
if te.Month() == 1 {
return te.Day() == 1 || te.Day() == 15
} else if te.Month() == 2 {
return te.Day() == 19
} else if te.Month() == 3 {
return te.Day() == 30
} else if te.Month() == 5 {
return te.Day() == 28
} else if te.Month() == 7 {
return te.Day() == 4
} else if te.Month() == 9 {
return te.Day() == 3
} else if te.Month() == 11 {
return te.Day() == 22
} else if te.Month() == 12 {
return te.Day() == 25
}
}
return false
}
// IsNYSEArcaHoliday returns that returns if a given time falls on a holiday.
func (d date) IsNYSEArcaHoliday(t time.Time) bool {
return d.IsNYSEHoliday(t)
}
// IsNASDAQHoliday returns if a date was a NASDAQ holiday day.
func (d date) IsNASDAQHoliday(t time.Time) bool {
return d.IsNYSEHoliday(t)
}
// Time returns a new time.Time for the given clock components.
func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time {
return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
}
func (d date) Date(year, month, day int, loc *time.Location) time.Time {
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc)
}
// On returns the clock components of clock (hour,minute,second) on the date components of d.
func (d date) On(clock, cd time.Time) time.Time {
tzAdjusted := cd.In(clock.Location())
return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
}
// NoonOn is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date.
func (d date) NoonOn(cd time.Time) time.Time {
return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location())
}
// Optional returns a pointer reference to a given time.
func (d date) Optional(t time.Time) *time.Time {
return &t
}
// IsWeekDay returns if the day is a monday->friday.
func (d date) IsWeekDay(day time.Weekday) bool {
return !d.IsWeekendDay(day)
}
// IsWeekendDay returns if the day is a monday->friday.
func (d date) IsWeekendDay(day time.Weekday) bool {
return day == time.Saturday || day == time.Sunday
}
// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
func (d date) Before(before, reference time.Time) bool {
tzAdjustedBefore := before.In(reference.Location())
if tzAdjustedBefore.Year() < reference.Year() {
return true
}
if tzAdjustedBefore.Month() < reference.Month() {
return true
}
return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day()
}
// NextMarketOpen returns the next market open after a given time.
func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time {
afterLocalized := after.In(openTime.Location())
todaysOpen := d.On(openTime, afterLocalized)
if isHoliday == nil {
isHoliday = defaultHolidayProvider
}
todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen)
if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay {
return todaysOpen
}
for cursorDay := 1; cursorDay < 7; cursorDay++ {
newDay := todaysOpen.AddDate(0, 0, cursorDay)
isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay)
if isValidTradingDay {
return d.On(openTime, newDay)
}
}
panic("Have exhausted day window looking for next market open.")
}
// NextMarketClose returns the next market close after a given time.
func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time {
afterLocalized := after.In(closeTime.Location())
if isHoliday == nil {
isHoliday = defaultHolidayProvider
}
todaysClose := d.On(closeTime, afterLocalized)
if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
return todaysClose
}
if afterLocalized.Equal(todaysClose) { //rare but it might happen.
return todaysClose
}
for cursorDay := 1; cursorDay < 6; cursorDay++ {
newDay := todaysClose.AddDate(0, 0, cursorDay)
if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) {
return d.On(closeTime, newDay)
}
}
panic("Have exhausted day window looking for next market close.")
}
// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates.
func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) {
startEastern := start.In(d.Eastern())
endEastern := end.In(d.Eastern())
startMarketOpen := d.On(marketOpen, startEastern)
startMarketClose := d.On(marketClose, startEastern)
if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) {
if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) {
if endEastern.Before(startMarketClose) {
seconds += int64(endEastern.Sub(startEastern) / time.Second)
} else {
seconds += int64(startMarketClose.Sub(startEastern) / time.Second)
}
}
}
cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday)
for d.Before(cursor, endEastern) {
if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) {
close := d.NextMarketClose(cursor, marketClose, isHoliday)
seconds += int64(close.Sub(cursor) / time.Second)
}
cursor = cursor.AddDate(0, 0, 1)
}
finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday)
finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday)
if endEastern.After(finalMarketOpen) {
if endEastern.Before(finalMarketClose) {
seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second)
} else {
seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second)
}
}
return
}
const (
_secondsPerHour = 60 * 60
_secondsPerDay = 60 * 60 * 24
)
func (d date) DiffDays(t1, t2 time.Time) (days int) {
t1n := t1.Unix()
t2n := t2.Unix()
diff := t2n - t1n //yields seconds
return int(diff / (_secondsPerDay))
}
func (d date) DiffHours(t1, t2 time.Time) (hours int) {
t1n := t1.Unix()
t2n := t2.Unix()
diff := t2n - t1n //yields seconds
return int(diff / (_secondsPerHour))
}
// NextDay returns the timestamp advanced a day.
func (d date) NextDay(ts time.Time) time.Time {
return ts.AddDate(0, 0, 1)
}
// NextHour returns the next timestamp on the hour.
func (d date) NextHour(ts time.Time) time.Time {
//advance a full hour ...
advanced := ts.Add(time.Hour)
minutes := time.Duration(advanced.Minute()) * time.Minute
final := advanced.Add(-minutes)
return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location())
}
// NextDayOfWeek returns the next instance of a given weekday after a given timestamp.
func (d date) NextDayOfWeek(after time.Time, dayOfWeek time.Weekday) time.Time {
afterWeekday := after.Weekday()
if afterWeekday == dayOfWeek {
return after.AddDate(0, 0, 7)
}
// 1 vs 5 ~ add 4 days
if afterWeekday < dayOfWeek {
dayDelta := int(dayOfWeek - afterWeekday)
return after.AddDate(0, 0, dayDelta)
}
// 5 vs 1, add 7-(5-1) ~ 3 days
dayDelta := 7 - int(afterWeekday-dayOfWeek)
return after.AddDate(0, 0, dayDelta)
}
// Start returns the earliest (min) time in a list of times.
func (d date) Start(times []time.Time) time.Time {
if len(times) == 0 {
return time.Time{}
}
start := times[0]
for _, t := range times[1:] {
if t.Before(start) {
start = t
}
}
return start
}
// Start returns the earliest (min) time in a list of times.
func (d date) End(times []time.Time) time.Time {
if len(times) == 0 {
return time.Time{}
}
end := times[0]
for _, t := range times[1:] {
if t.After(end) {
end = t
}
}
return end
}

17
date_posix.go Normal file
View file

@ -0,0 +1,17 @@
// +build !windows
package chart
import "time"
// Eastern returns the eastern timezone.
func (d date) Eastern() *time.Location {
if _eastern == nil {
_easternLock.Lock()
defer _easternLock.Unlock()
if _eastern == nil {
_eastern, _ = time.LoadLocation("America/New_York")
}
}
return _eastern
}

288
date_test.go Normal file
View file

@ -0,0 +1,288 @@
package chart
import (
"testing"
"time"
assert "github.com/blendlabs/go-assert"
)
func parse(v string) time.Time {
ts, _ := time.Parse("2006-01-02", v)
return ts
}
func TestDateTime(t *testing.T) {
assert := assert.New(t)
ts := Date.Time(5, 6, 7, 8, time.UTC)
assert.Equal(05, ts.Hour())
assert.Equal(06, ts.Minute())
assert.Equal(07, ts.Second())
assert.Equal(8, ts.Nanosecond())
assert.Equal(time.UTC, ts.Location())
}
func TestDateDate(t *testing.T) {
assert := assert.New(t)
ts := Date.Date(2015, 5, 6, time.UTC)
assert.Equal(2015, ts.Year())
assert.Equal(5, ts.Month())
assert.Equal(6, ts.Day())
assert.Equal(time.UTC, ts.Location())
}
func TestDateOn(t *testing.T) {
assert := assert.New(t)
ts := Date.On(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.Eastern()))
assert.Equal(2016, ts.Year())
assert.Equal(6, ts.Month())
assert.Equal(7, ts.Day())
assert.Equal(5, ts.Hour())
assert.Equal(4, ts.Minute())
assert.Equal(3, ts.Second())
assert.Equal(2, ts.Nanosecond())
assert.Equal(time.UTC, ts.Location())
}
func TestDateNoonOn(t *testing.T) {
assert := assert.New(t)
noon := Date.NoonOn(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC))
assert.Equal(2016, noon.Year())
assert.Equal(4, noon.Month())
assert.Equal(3, noon.Day())
assert.Equal(12, noon.Hour())
assert.Equal(0, noon.Minute())
assert.Equal(time.UTC, noon.Location())
}
func TestDateBefore(t *testing.T) {
assert := assert.New(t)
assert.True(Date.Before(parse("2015-07-02"), parse("2016-07-01")))
assert.True(Date.Before(parse("2016-06-01"), parse("2016-07-01")))
assert.True(Date.Before(parse("2016-07-01"), parse("2016-07-02")))
assert.False(Date.Before(parse("2016-07-01"), parse("2016-07-01")))
assert.False(Date.Before(parse("2016-07-03"), parse("2016-07-01")))
assert.False(Date.Before(parse("2016-08-03"), parse("2016-07-01")))
assert.False(Date.Before(parse("2017-08-03"), parse("2016-07-01")))
}
func TestDateBeforeHandlesTimezones(t *testing.T) {
assert := assert.New(t)
tuesdayUTC := time.Date(2016, 8, 02, 22, 00, 0, 0, time.UTC)
mondayUTC := time.Date(2016, 8, 01, 1, 00, 0, 0, time.UTC)
sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.Eastern())
assert.True(Date.Before(sundayEST, tuesdayUTC))
assert.False(Date.Before(sundayEST, mondayUTC))
}
func TestNextMarketOpen(t *testing.T) {
assert := assert.New(t)
beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Date.Eastern())
todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Date.Eastern())
tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Date.Eastern())
mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Date.Eastern())
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
assert.True(todayOpen.Equal(Date.NextMarketOpen(beforeOpen, NYSEOpen(), Date.IsNYSEHoliday)))
assert.True(tomorrowOpen.Equal(Date.NextMarketOpen(afterOpen, NYSEOpen(), Date.IsNYSEHoliday)))
assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen(), Date.IsNYSEHoliday)))
assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen(), Date.IsNYSEHoliday)))
assert.Equal(Date.Eastern(), todayOpen.Location())
assert.Equal(Date.Eastern(), tomorrowOpen.Location())
assert.Equal(Date.Eastern(), mondayOpen.Location())
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern())
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
assert.True(shouldbe.Equal(Date.NextMarketOpen(testRegression, NYSEOpen(), Date.IsNYSEHoliday)))
}
func TestNextMarketClose(t *testing.T) {
assert := assert.New(t)
beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Date.Eastern())
todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Date.Eastern())
afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Date.Eastern())
tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Date.Eastern())
afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Date.Eastern())
mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Date.Eastern())
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
assert.True(todayClose.Equal(Date.NextMarketClose(beforeClose, NYSEClose(), Date.IsNYSEHoliday)))
assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose(), Date.IsNYSEHoliday)))
assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose(), Date.IsNYSEHoliday)))
assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose(), Date.IsNYSEHoliday)))
assert.Equal(Date.Eastern(), todayClose.Location())
assert.Equal(Date.Eastern(), tomorrowClose.Location())
assert.Equal(Date.Eastern(), mondayClose.Location())
}
func TestCalculateMarketSecondsBetween(t *testing.T) {
assert := assert.New(t)
start := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
end := time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern())
shouldbe := 5 * 6.5 * 60 * 60
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
}
func TestCalculateMarketSecondsBetween1D(t *testing.T) {
assert := assert.New(t)
start := time.Date(2016, 07, 22, 9, 45, 0, 0, Date.Eastern())
end := time.Date(2016, 07, 22, 15, 45, 0, 0, Date.Eastern())
shouldbe := 6 * 60 * 60
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
}
func TestCalculateMarketSecondsBetweenLTM(t *testing.T) {
assert := assert.New(t)
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
end := time.Date(2016, 07, 01, 9, 30, 0, 0, Date.Eastern())
shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year.
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
}
func TestDateNextHour(t *testing.T) {
assert := assert.New(t)
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
next := Date.NextHour(start)
assert.Equal(2015, next.Year())
assert.Equal(07, next.Month())
assert.Equal(01, next.Day())
assert.Equal(10, next.Hour())
assert.Equal(00, next.Minute())
next = Date.NextHour(next)
assert.Equal(11, next.Hour())
next = Date.NextHour(next)
assert.Equal(12, next.Hour())
}
func TestDateNextDayOfWeek(t *testing.T) {
assert := assert.New(t)
weds := Date.Date(2016, 8, 10, time.UTC)
fri := Date.Date(2016, 8, 12, time.UTC)
sun := Date.Date(2016, 8, 14, time.UTC)
mon := Date.Date(2016, 8, 15, time.UTC)
weds2 := Date.Date(2016, 8, 17, time.UTC)
nextFri := Date.NextDayOfWeek(weds, time.Friday)
nextSunday := Date.NextDayOfWeek(weds, time.Sunday)
nextMonday := Date.NextDayOfWeek(weds, time.Monday)
nextWeds := Date.NextDayOfWeek(weds, time.Wednesday)
assert.Equal(fri.Year(), nextFri.Year())
assert.Equal(fri.Month(), nextFri.Month())
assert.Equal(fri.Day(), nextFri.Day())
assert.Equal(sun.Year(), nextSunday.Year())
assert.Equal(sun.Month(), nextSunday.Month())
assert.Equal(sun.Day(), nextSunday.Day())
assert.Equal(mon.Year(), nextMonday.Year())
assert.Equal(mon.Month(), nextMonday.Month())
assert.Equal(mon.Day(), nextMonday.Day())
assert.Equal(weds2.Year(), nextWeds.Year())
assert.Equal(weds2.Month(), nextWeds.Month())
assert.Equal(weds2.Day(), nextWeds.Day())
assert.Equal(time.UTC, nextFri.Location())
assert.Equal(time.UTC, nextSunday.Location())
assert.Equal(time.UTC, nextMonday.Location())
}
func TestDateIsNYSEHoliday(t *testing.T) {
assert := assert.New(t)
cursor := time.Date(2013, 01, 01, 0, 0, 0, 0, time.UTC)
end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
var holidays int
for Date.Before(cursor, end) {
if Date.IsNYSEHoliday(cursor) {
holidays++
}
cursor = cursor.AddDate(0, 0, 1)
}
assert.Equal(holidays, 55)
}
func TestTimeStart(t *testing.T) {
assert := assert.New(t)
times := []time.Time{
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -1),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -5),
}
assert.InTimeDelta(Date.Start(times), times[4], time.Millisecond)
}
func TestTimeEnd(t *testing.T) {
assert := assert.New(t)
times := []time.Time{
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -1),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -5),
}
assert.InTimeDelta(Date.End(times), times[2], time.Millisecond)
}
func TestDateDiffDays(t *testing.T) {
assert := assert.New(t)
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
t2 := time.Date(2017, 01, 10, 3, 0, 0, 0, time.UTC)
t3 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
assert.Equal(48, Date.DiffDays(t2, t1))
assert.Equal(2, Date.DiffDays(t3, t1)) // technically we should round down.
}
func TestDateDiffHours(t *testing.T) {
assert := assert.New(t)
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
t2 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
t3 := time.Date(2017, 02, 28, 12, 0, 0, 0, time.UTC)
assert.Equal(68, Date.DiffHours(t2, t1))
assert.Equal(24, Date.DiffHours(t1, t3))
}

17
date_windows.go Normal file
View file

@ -0,0 +1,17 @@
// +build windows
package chart
import "time"
// Eastern returns the eastern timezone.
func (d date) Eastern() *time.Location {
if _eastern == nil {
_easternLock.Lock()
defer _easternLock.Unlock()
if _eastern == nil {
_eastern, _ = time.LoadLocation("EST")
}
}
return _eastern
}

View file

@ -1,5 +1,12 @@
package chart
import (
"sync"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/drawing"
)
const (
// DefaultChartHeight is the default chart height.
DefaultChartHeight = 400
@ -75,6 +82,96 @@ const (
DefaultBarWidth = 50
)
var (
// ColorWhite is white.
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// ColorBlue is the basic theme blue color.
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
// ColorCyan is the basic theme cyan color.
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
// ColorGreen is the basic theme green color.
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
// ColorRed is the basic theme red color.
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
// ColorOrange is the basic theme orange color.
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
// ColorYellow is the basic theme yellow color.
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
// ColorBlack is the basic theme black color.
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
// ColorLightGray is the basic theme light gray color.
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
// ColorAlternateBlue is a alternate theme color.
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
// ColorAlternateGreen is a alternate theme color.
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
// ColorAlternateGray is a alternate theme color.
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
// ColorAlternateYellow is a alternate theme color.
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
// ColorAlternateLightGray is a alternate theme color.
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
// ColorTransparent is a transparent (alpha zero) color.
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
)
var (
// DefaultBackgroundColor is the default chart background color.
// It is equivalent to css color:white.
DefaultBackgroundColor = ColorWhite
// DefaultBackgroundStrokeColor is the default chart border color.
// It is equivalent to color:white.
DefaultBackgroundStrokeColor = ColorWhite
// DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white.
DefaultCanvasColor = ColorWhite
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
// It is equivalent to css color:white.
DefaultCanvasStrokeColor = ColorWhite
// DefaultTextColor is the default chart text color.
// It is equivalent to #333333.
DefaultTextColor = ColorBlack
// DefaultAxisColor is the default chart axis line color.
// It is equivalent to #333333.
DefaultAxisColor = ColorBlack
// DefaultStrokeColor is the default chart border color.
// It is equivalent to #efefef.
DefaultStrokeColor = ColorLightGray
// DefaultFillColor is the default fill color.
// It is equivalent to #0074d9.
DefaultFillColor = ColorBlue
// DefaultAnnotationFillColor is the default annotation background color.
DefaultAnnotationFillColor = ColorWhite
// DefaultGridLineColor is the default grid line color.
DefaultGridLineColor = ColorLightGray
)
var (
// DefaultColors are a couple default series colors.
DefaultColors = []drawing.Color{
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
// DefaultAlternateColors are a couple alternate colors.
DefaultAlternateColors = []drawing.Color{
ColorAlternateBlue,
ColorAlternateGreen,
ColorAlternateGray,
ColorAlternateYellow,
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
)
var (
// DashArrayDots is a dash array that represents '....' style stroke dashes.
DashArrayDots = []int{1, 1}
@ -86,18 +183,49 @@ var (
DashArrayDashesLarge = []int{10, 10}
)
// NewColor returns a new color.
func NewColor(r, g, b, a uint8) drawing.Color {
return drawing.Color{R: r, G: g, B: b, A: a}
}
// GetDefaultColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetDefaultColor(index int) drawing.Color {
finalIndex := index % len(DefaultColors)
return DefaultColors[finalIndex]
}
// GetAlternateColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetAlternateColor(index int) drawing.Color {
finalIndex := index % len(DefaultAlternateColors)
return DefaultAlternateColors[finalIndex]
}
var (
// DefaultAnnotationPadding is the padding around an annotation.
DefaultAnnotationPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
// DefaultBackgroundPadding is the default canvas padding config.
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
)
const (
// ContentTypePNG is the png mime type.
ContentTypePNG = "image/png"
// ContentTypeSVG is the svg mime type.
ContentTypeSVG = "image/svg+xml"
var (
_defaultFontLock sync.Mutex
_defaultFont *truetype.Font
)
// GetDefaultFont returns the default font (Roboto-Medium).
func GetDefaultFont() (*truetype.Font, error) {
if _defaultFont == nil {
_defaultFontLock.Lock()
defer _defaultFontLock.Unlock()
if _defaultFont == nil {
font, err := truetype.Parse(roboto)
if err != nil {
return nil, err
}
_defaultFont = font
}
}
return _defaultFont, nil
}

View file

@ -1,315 +0,0 @@
package chart
import (
"errors"
"fmt"
"io"
"github.com/golang/freetype/truetype"
)
// DonutChart is a chart that draws sections of a circle based on percentages with an hole.
type DonutChart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
Background Style
Canvas Style
SliceStyle Style
Font *truetype.Font
defaultFont *truetype.Font
Values []Value
Elements []Renderable
}
// GetDPI returns the dpi for the chart.
func (pc DonutChart) GetDPI(defaults ...float64) float64 {
if pc.DPI == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultDPI
}
return pc.DPI
}
// GetFont returns the text font.
func (pc DonutChart) GetFont() *truetype.Font {
if pc.Font == nil {
return pc.defaultFont
}
return pc.Font
}
// GetWidth returns the chart width or the default value.
func (pc DonutChart) GetWidth() int {
if pc.Width == 0 {
return DefaultChartWidth
}
return pc.Width
}
// GetHeight returns the chart height or the default value.
func (pc DonutChart) GetHeight() int {
if pc.Height == 0 {
return DefaultChartWidth
}
return pc.Height
}
// Render renders the chart with the given renderer to the given io.Writer.
func (pc DonutChart) Render(rp RendererProvider, w io.Writer) error {
if len(pc.Values) == 0 {
return errors.New("please provide at least one value")
}
r, err := rp(pc.GetWidth(), pc.GetHeight())
if err != nil {
return err
}
if pc.Font == nil {
defaultFont, err := GetDefaultFont()
if err != nil {
return err
}
pc.defaultFont = defaultFont
}
r.SetDPI(pc.GetDPI(DefaultDPI))
canvasBox := pc.getDefaultCanvasBox()
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
pc.drawBackground(r)
pc.drawCanvas(r, canvasBox)
finalValues, err := pc.finalizeValues(pc.Values)
if err != nil {
return err
}
pc.drawSlices(r, canvasBox, finalValues)
pc.drawTitle(r)
for _, a := range pc.Elements {
a(r, canvasBox, pc.styleDefaultsElements())
}
return r.Save(w)
}
func (pc DonutChart) drawBackground(r Renderer) {
Draw.Box(r, Box{
Right: pc.GetWidth(),
Bottom: pc.GetHeight(),
}, pc.getBackgroundStyle())
}
func (pc DonutChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, pc.getCanvasStyle())
}
func (pc DonutChart) drawTitle(r Renderer) {
if len(pc.Title) > 0 && !pc.TitleStyle.Hidden {
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
}
}
func (pc DonutChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
cx, cy := canvasBox.Center()
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
radius := float64(diameter>>1) / 1.1
labelRadius := (radius * 2.83) / 3.0
// draw the donut slices
var rads, delta, delta2, total float64
var lx, ly int
if len(values) == 1 {
pc.styleDonutChartValue(0).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.Circle(radius, cx, cy)
} else {
for index, v := range values {
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy)
rads = PercentToRadians(total)
delta = PercentToRadians(v.Value)
r.ArcTo(cx, cy, (radius / 1.25), (radius / 1.25), rads, delta)
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
total = total + v.Value
}
}
//making the donut hole
v := Value{Value: 100, Label: "center"}
styletemp := pc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite, StrokeWidth: 4.0, FillColor: ColorWhite, FontColor: ColorWhite, //Font: pc.GetFont(),//FontSize: pc.getScaledFontSize(),
})
v.Style.InheritFrom(styletemp).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.ArcTo(cx, cy, (radius / 3.5), (radius / 3.5), DegreesToRadians(0), DegreesToRadians(359))
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
// draw the labels
total = 0
for index, v := range values {
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2)
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
ly = ly + (tb.Height() >> 1)
r.Text(v.Label, lx, ly)
}
total = total + v.Value
}
}
func (pc DonutChart) finalizeValues(values []Value) ([]Value, error) {
finalValues := Values(values).Normalize()
if len(finalValues) == 0 {
return nil, fmt.Errorf("donut chart must contain at least (1) non-zero value")
}
return finalValues, nil
}
func (pc DonutChart) getDefaultCanvasBox() Box {
return pc.Box()
}
func (pc DonutChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
square := Box{
Right: circleDiameter,
Bottom: circleDiameter,
}
return canvasBox.Fit(square)
}
func (pc DonutChart) getBackgroundStyle() Style {
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
}
func (pc DonutChart) getCanvasStyle() Style {
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
}
func (pc DonutChart) styleDefaultsCanvas() Style {
return Style{
FillColor: pc.GetColorPalette().CanvasColor(),
StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc DonutChart) styleDefaultsDonutChartValue() Style {
return Style{
StrokeColor: pc.GetColorPalette().TextColor(),
StrokeWidth: 4.0,
FillColor: pc.GetColorPalette().TextColor(),
}
}
func (pc DonutChart) styleDonutChartValue(index int) Style {
return pc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite,
StrokeWidth: 4.0,
FillColor: pc.GetColorPalette().GetSeriesColor(index),
FontSize: pc.getScaledFontSize(),
FontColor: pc.GetColorPalette().TextColor(),
Font: pc.GetFont(),
})
}
func (pc DonutChart) getScaledFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 {
return 48.0
} else if effectiveDimension >= 1024 {
return 24.0
} else if effectiveDimension > 512 {
return 18.0
} else if effectiveDimension > 256 {
return 12.0
}
return 10.0
}
func (pc DonutChart) styleDefaultsBackground() Style {
return Style{
FillColor: pc.GetColorPalette().BackgroundColor(),
StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc DonutChart) styleDefaultsElements() Style {
return Style{
Font: pc.GetFont(),
}
}
func (pc DonutChart) styleDefaultsTitle() Style {
return pc.TitleStyle.InheritFrom(Style{
FontColor: pc.GetColorPalette().TextColor(),
Font: pc.GetFont(),
FontSize: pc.getTitleFontSize(),
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
})
}
func (pc DonutChart) getTitleFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 {
return 48
} else if effectiveDimension >= 1024 {
return 24
} else if effectiveDimension >= 512 {
return 18
} else if effectiveDimension >= 256 {
return 12
}
return 10
}
// GetColorPalette returns the color palette for the chart.
func (pc DonutChart) GetColorPalette() ColorPalette {
if pc.ColorPalette != nil {
return pc.ColorPalette
}
return AlternateColorPalette
}
// Box returns the chart bounds as a box.
func (pc DonutChart) Box() Box {
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
Right: pc.GetWidth() - dpr,
Bottom: pc.GetHeight() - dpb,
}
}

View file

@ -1,69 +0,0 @@
package chart
import (
"bytes"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestDonutChart(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 10, Label: "Blue"},
{Value: 9, Label: "Green"},
{Value: 8, Label: "Gray"},
{Value: 7, Label: "Orange"},
{Value: 6, Label: "HEANG"},
{Value: 5, Label: "??"},
{Value: 2, Label: "!!"},
},
}
b := bytes.NewBuffer([]byte{})
pie.Render(PNG, b)
testutil.AssertNotZero(t, b.Len())
}
func TestDonutChartDropsZeroValues(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 5, Label: "Blue"},
{Value: 5, Label: "Green"},
{Value: 0, Label: "Gray"},
},
}
b := bytes.NewBuffer([]byte{})
err := pie.Render(PNG, b)
testutil.AssertNil(t, err)
}
func TestDonutChartAllZeroValues(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 0, Label: "Blue"},
{Value: 0, Label: "Green"},
{Value: 0, Label: "Gray"},
},
}
b := bytes.NewBuffer([]byte{})
err := pie.Render(PNG, b)
testutil.AssertNotNil(t, err)
}

51
draw.go
View file

@ -1,8 +1,6 @@
package chart
import (
"math"
)
import "math"
var (
// Draw contains helpers for drawing common objects.
@ -12,7 +10,7 @@ var (
type draw struct{}
// LineSeries draws a line series with a renderer.
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
if vs.Len() == 0 {
return
}
@ -20,7 +18,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
cb := canvasBox.Bottom
cl := canvasBox.Left
v0x, v0y := vs.GetValues(0)
v0x, v0y := vs.GetValue(0)
x0 := cl + xrange.Translate(v0x)
y0 := cb - yrange.Translate(v0y)
@ -33,13 +31,13 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
vx, vy = vs.GetValue(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
}
r.LineTo(x, MinInt(cb, cb-yv0))
r.LineTo(x0, MinInt(cb, cb-yv0))
r.LineTo(x, Math.MinInt(cb, cb-yv0))
r.LineTo(x0, Math.MinInt(cb, cb-yv0))
r.LineTo(x0, y0)
r.Fill()
}
@ -49,7 +47,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
vx, vy = vs.GetValue(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
@ -58,34 +56,23 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
}
if style.ShouldDrawDot() {
defaultDotWidth := style.GetDotWidth()
dotWidth := style.GetDotWidth()
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
for i := 0; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
vx, vy = vs.GetValue(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
dotWidth := defaultDotWidth
if style.DotWidthProvider != nil {
dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
}
if style.DotColorProvider != nil {
dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
r.SetFillColor(dotColor)
r.SetStrokeColor(dotColor)
}
r.Circle(dotWidth, x, y)
r.FillStroke()
}
}
}
// BoundedSeries draws a series that implements BoundedValuesProvider.
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
// BoundedSeries draws a series that implements BoundedValueProvider.
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
drawOffsetIndex := 0
if len(drawOffsetIndexes) > 0 {
drawOffsetIndex = drawOffsetIndexes[0]
@ -94,7 +81,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
cb := canvasBox.Bottom
cl := canvasBox.Left
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
v0x, v0y1, v0y2 := bbs.GetBoundedValue(0)
x0 := cl + xrange.Translate(v0x)
y0 := cb - yrange.Translate(v0y1)
@ -109,7 +96,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
style.GetFillAndStrokeOptions().WriteToRenderer(r)
r.MoveTo(x0, y0)
for i := 1; i < bbs.Len(); i++ {
vx, vy1, vy2 = bbs.GetBoundedValues(i)
vx, vy1, vy2 = bbs.GetBoundedValue(i)
xvalues[i] = vx
y2values[i] = vy2
@ -135,7 +122,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
}
// HistogramSeries draws a value provider as boxes from 0.
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
if vs.Len() == 0 {
return
}
@ -152,7 +139,7 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
//foreach datapoint, draw a box.
for index := 0; index < seriesLength; index++ {
vx, vy := vs.GetValues(index)
vx, vy := vs.GetValue(index)
y0 := yrange.Translate(0)
x := cl + xrange.Translate(vx)
y := yrange.Translate(vy)
@ -296,10 +283,8 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
switch style.GetTextVerticalAlign() {
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
y = y - linesBox.Height()
case TextVerticalAlignMiddle:
y = y + (box.Height() >> 1) - (linesBox.Height() >> 1)
case TextVerticalAlignMiddleBaseline:
y = y + (box.Height() >> 1) - linesBox.Height()
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
y = (y - linesBox.Height()) >> 1
}
var tx, ty int

View file

@ -2,46 +2,27 @@ package drawing
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// Basic Colors from:
// https://www.w3.org/wiki/CSS/Properties/color/keywords
var (
// ColorTransparent is a fully transparent color.
ColorTransparent = Color{R: 255, G: 255, B: 255, A: 0}
ColorTransparent = Color{}
// ColorWhite is white.
ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
// ColorBlack is black.
ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
// ColorRed is red.
ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
// ColorGreen is green.
ColorGreen = Color{R: 0, G: 128, B: 0, A: 255}
ColorGreen = Color{R: 0, G: 255, B: 0, A: 255}
// ColorBlue is blue.
ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
// ColorSilver is a known color.
ColorSilver = Color{R: 192, G: 192, B: 192, A: 255}
// ColorMaroon is a known color.
ColorMaroon = Color{R: 128, G: 0, B: 0, A: 255}
// ColorPurple is a known color.
ColorPurple = Color{R: 128, G: 0, B: 128, A: 255}
// ColorFuchsia is a known color.
ColorFuchsia = Color{R: 255, G: 0, B: 255, A: 255}
// ColorLime is a known color.
ColorLime = Color{R: 0, G: 255, B: 0, A: 255}
// ColorOlive is a known color.
ColorOlive = Color{R: 128, G: 128, B: 0, A: 255}
// ColorYellow is a known color.
ColorYellow = Color{R: 255, G: 255, B: 0, A: 255}
// ColorNavy is a known color.
ColorNavy = Color{R: 0, G: 0, B: 128, A: 255}
// ColorTeal is a known color.
ColorTeal = Color{R: 0, G: 128, B: 128, A: 255}
// ColorAqua is a known color.
ColorAqua = Color{R: 0, G: 255, B: 255, A: 255}
)
func parseHex(hex string) uint8 {
@ -49,97 +30,8 @@ func parseHex(hex string) uint8 {
return uint8(v)
}
// ParseColor parses a color from a string.
func ParseColor(rawColor string) Color {
if strings.HasPrefix(rawColor, "rgba") {
return ColorFromRGBA(rawColor)
}
if strings.HasPrefix(rawColor, "rgb") {
return ColorFromRGB(rawColor)
}
if strings.HasPrefix(rawColor, "#") {
return ColorFromHex(rawColor)
}
return ColorFromKnown(rawColor)
}
var rgbaexpr = regexp.MustCompile(`rgba\((?P<R>.+),(?P<G>.+),(?P<B>.+),(?P<A>.+)\)`)
// ColorFromRGBA returns a color from an `rgba()` css function.
func ColorFromRGBA(rgba string) (output Color) {
values := rgbaexpr.FindStringSubmatch(rgba)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
case "A":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseFloat(value, 32)
if parsed > 1 {
parsed = 1
} else if parsed < 0 {
parsed = 0
}
output.A = uint8(parsed * 255)
}
}
return
}
var rgbexpr = regexp.MustCompile(`rgb\((?P<R>.+),(?P<G>.+),(?P<B>.+)\)`)
// ColorFromRGB returns a color from an `rgb()` css function.
func ColorFromRGB(rgb string) (output Color) {
output.A = 255
values := rgbexpr.FindStringSubmatch(rgb)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
}
}
return
}
// ColorFromHex returns a color from a css hex code.
//
// NOTE: it will trim a leading '#' character if present.
func ColorFromHex(hex string) Color {
if strings.HasPrefix(hex, "#") {
hex = strings.TrimPrefix(hex, "#")
}
var c Color
if len(hex) == 3 {
c.R = parseHex(string(hex[0])) * 0x11
@ -154,46 +46,6 @@ func ColorFromHex(hex string) Color {
return c
}
// ColorFromKnown returns an internal color from a known (basic) color name.
func ColorFromKnown(known string) Color {
switch strings.ToLower(known) {
case "transparent":
return ColorTransparent
case "white":
return ColorWhite
case "black":
return ColorBlack
case "red":
return ColorRed
case "blue":
return ColorBlue
case "green":
return ColorGreen
case "silver":
return ColorSilver
case "maroon":
return ColorMaroon
case "purple":
return ColorPurple
case "fuchsia":
return ColorFuchsia
case "lime":
return ColorLime
case "olive":
return ColorOlive
case "yellow":
return ColorYellow
case "navy":
return ColorNavy
case "teal":
return ColorTeal
case "aqua":
return ColorAqua
default:
return Color{}
}
}
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
fa := float64(a) / 255.0
@ -205,11 +57,6 @@ func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
return c
}
// ColorChannelFromFloat returns a normalized byte from a given float value.
func ColorChannelFromFloat(v float64) uint8 {
return uint8(v * 255)
}
// Color is our internal color type because color.Color is bullshit.
type Color struct {
R, G, B, A uint8

View file

@ -1,114 +1,53 @@
package drawing
import (
"fmt"
"testing"
"image/color"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
)
func TestColorFromHex(t *testing.T) {
assert := assert.New(t)
white := ColorFromHex("FFFFFF")
testutil.AssertEqual(t, ColorWhite, white)
assert.Equal(ColorWhite, white)
shortWhite := ColorFromHex("FFF")
testutil.AssertEqual(t, ColorWhite, shortWhite)
assert.Equal(ColorWhite, shortWhite)
black := ColorFromHex("000000")
testutil.AssertEqual(t, ColorBlack, black)
assert.Equal(ColorBlack, black)
shortBlack := ColorFromHex("000")
testutil.AssertEqual(t, ColorBlack, shortBlack)
assert.Equal(ColorBlack, shortBlack)
red := ColorFromHex("FF0000")
testutil.AssertEqual(t, ColorRed, red)
assert.Equal(ColorRed, red)
shortRed := ColorFromHex("F00")
testutil.AssertEqual(t, ColorRed, shortRed)
assert.Equal(ColorRed, shortRed)
green := ColorFromHex("008000")
testutil.AssertEqual(t, ColorGreen, green)
green := ColorFromHex("00FF00")
assert.Equal(ColorGreen, green)
// shortGreen := ColorFromHex("0F0")
// testutil.AssertEqual(t, ColorGreen, shortGreen)
shortGreen := ColorFromHex("0F0")
assert.Equal(ColorGreen, shortGreen)
blue := ColorFromHex("0000FF")
testutil.AssertEqual(t, ColorBlue, blue)
assert.Equal(ColorBlue, blue)
shortBlue := ColorFromHex("00F")
testutil.AssertEqual(t, ColorBlue, shortBlue)
}
func TestColorFromHex_handlesHash(t *testing.T) {
withHash := ColorFromHex("#FF0000")
testutil.AssertEqual(t, ColorRed, withHash)
withoutHash := ColorFromHex("#FF0000")
testutil.AssertEqual(t, ColorRed, withoutHash)
assert.Equal(ColorBlue, shortBlue)
}
func TestColorFromAlphaMixedRGBA(t *testing.T) {
assert := assert.New(t)
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
testutil.AssertTrue(t, black.Equals(ColorBlack), black.String())
assert.True(black.Equals(ColorBlack), black.String())
white := ColorFromAlphaMixedRGBA(color.White.RGBA())
testutil.AssertTrue(t, white.Equals(ColorWhite), white.String())
}
func Test_ColorFromRGBA(t *testing.T) {
value := "rgba(192, 192, 192, 1.0)"
parsed := ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
value = "rgba(192,192,192,1.0)"
parsed = ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
value = "rgba(192,192,192,1.5)"
parsed = ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
}
func TestParseColor(t *testing.T) {
testCases := [...]struct {
Input string
Expected Color
}{
{"", Color{}},
{"white", ColorWhite},
{"WHITE", ColorWhite}, // caps!
{"black", ColorBlack},
{"red", ColorRed},
{"green", ColorGreen},
{"blue", ColorBlue},
{"silver", ColorSilver},
{"maroon", ColorMaroon},
{"purple", ColorPurple},
{"fuchsia", ColorFuchsia},
{"lime", ColorLime},
{"olive", ColorOlive},
{"yellow", ColorYellow},
{"navy", ColorNavy},
{"teal", ColorTeal},
{"aqua", ColorAqua},
{"rgba(192, 192, 192, 1.0)", ColorSilver},
{"rgba(192,192,192,1.0)", ColorSilver},
{"rgb(192, 192, 192)", ColorSilver},
{"rgb(192,192,192)", ColorSilver},
{"#FF0000", ColorRed},
{"#008000", ColorGreen},
{"#0000FF", ColorBlue},
{"#F00", ColorRed},
{"#080", Color{0, 136, 0, 255}},
{"#00F", ColorBlue},
}
for index, tc := range testCases {
actual := ParseColor(tc.Input)
testutil.AssertEqual(t, tc.Expected, actual, fmt.Sprintf("test case: %d -> %s", index, tc.Input))
}
assert.True(white.Equals(ColorWhite), white.String())
}

View file

@ -3,7 +3,7 @@ package drawing
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
assert "github.com/blendlabs/go-assert"
)
type point struct {
@ -23,7 +23,7 @@ func (ml mockLine) Len() int {
}
func TestTraceQuad(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
// Quad
// x1, y1, cpx1, cpy2, x2, y2 float64
@ -31,5 +31,5 @@ func TestTraceQuad(t *testing.T) {
quad := []float64{10, 20, 20, 20, 20, 10}
liner := &mockLine{}
TraceQuad(liner, quad, 0.5)
testutil.AssertNotZero(t, liner.Len())
assert.NotZero(liner.Len())
}

View file

@ -7,13 +7,6 @@ const (
DefaultEMAPeriod = 12
)
// Interface Assertions.
var (
_ Series = (*EMASeries)(nil)
_ FirstValuesProvider = (*EMASeries)(nil)
_ LastValuesProvider = (*EMASeries)(nil)
)
// EMASeries is a computed series.
type EMASeries struct {
Name string
@ -21,7 +14,7 @@ type EMASeries struct {
YAxis YAxisType
Period int
InnerSeries ValuesProvider
InnerSeries ValueProvider
cache []float64
}
@ -59,36 +52,23 @@ func (ema EMASeries) GetSigma() float64 {
return 2.0 / (float64(ema.GetPeriod()) + 1)
}
// GetValues gets a value at a given index.
func (ema *EMASeries) GetValues(index int) (x, y float64) {
// GetValue gets a value at a given index.
func (ema *EMASeries) GetValue(index int) (x, y float64) {
if ema.InnerSeries == nil {
return
}
if len(ema.cache) == 0 {
ema.ensureCachedValues()
}
vx, _ := ema.InnerSeries.GetValues(index)
vx, _ := ema.InnerSeries.GetValue(index)
x = vx
y = ema.cache[index]
return
}
// GetFirstValues computes the first moving average value.
func (ema *EMASeries) GetFirstValues() (x, y float64) {
if ema.InnerSeries == nil {
return
}
if len(ema.cache) == 0 {
ema.ensureCachedValues()
}
x, _ = ema.InnerSeries.GetValues(0)
y = ema.cache[0]
return
}
// GetLastValues computes the last moving average value but walking back window size samples,
// GetLastValue computes the last moving average value but walking back window size samples,
// and recomputing the last moving average chunk.
func (ema *EMASeries) GetLastValues() (x, y float64) {
func (ema *EMASeries) GetLastValue() (x, y float64) {
if ema.InnerSeries == nil {
return
}
@ -96,7 +76,7 @@ func (ema *EMASeries) GetLastValues() (x, y float64) {
ema.ensureCachedValues()
}
lastIndex := ema.InnerSeries.Len() - 1
x, _ = ema.InnerSeries.GetValues(lastIndex)
x, _ = ema.InnerSeries.GetValue(lastIndex)
y = ema.cache[lastIndex]
return
}
@ -106,7 +86,7 @@ func (ema *EMASeries) ensureCachedValues() {
ema.cache = make([]float64, seriesLength)
sigma := ema.GetSigma()
for x := 0; x < seriesLength; x++ {
_, y := ema.InnerSeries.GetValues(x)
_, y := ema.InnerSeries.GetValue(x)
if x == 0 {
ema.cache[x] = y
continue

View file

@ -3,11 +3,11 @@ package chart
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/blendlabs/go-assert"
)
var (
emaXValues = LinearRange(1.0, 50.0)
emaXValues = Sequence.Float64(1.0, 50.0)
emaYValues = []float64{
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
@ -73,13 +73,13 @@ var (
)
func TestEMASeries(t *testing.T) {
// replaced new assertions helper
assert := assert.New(t)
mockSeries := mockValuesProvider{
mockSeries := mockValueProvider{
emaXValues,
emaYValues,
}
testutil.AssertEqual(t, 50, mockSeries.Len())
assert.Equal(50, mockSeries.Len())
ema := &EMASeries{
InnerSeries: mockSeries,
@ -87,19 +87,19 @@ func TestEMASeries(t *testing.T) {
}
sig := ema.GetSigma()
testutil.AssertEqual(t, 2.0/(26.0+1), sig)
assert.Equal(2.0/(26.0+1), sig)
var yvalues []float64
for x := 0; x < ema.Len(); x++ {
_, y := ema.GetValues(x)
_, y := ema.GetValue(x)
yvalues = append(yvalues, y)
}
for index, yv := range yvalues {
testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta)
assert.InDelta(yv, emaExpected[index], emaDelta)
}
lvx, lvy := ema.GetLastValues()
testutil.AssertEqual(t, 50.0, lvx)
testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta)
lvx, lvy := ema.GetLastValue()
assert.Equal(50.0, lvx)
assert.InDelta(lvy, emaExpected[49], emaDelta)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,35 +0,0 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
graph := chart.BarChart{
Title: "Test Bar Chart",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
BarWidth: 60,
Bars: []chart.Value{
{Value: 5.25, Label: "Blue"},
{Value: 4.88, Label: "Green"},
{Value: 4.74, Label: "Gray"},
{Value: 3.22, Label: "Orange"},
{Value: 3, Label: "Test"},
{Value: 2.27, Label: "??"},
{Value: 1, Label: "!!"},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -1,62 +0,0 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func main() {
profitStyle := chart.Style{
FillColor: drawing.ColorFromHex("13c158"),
StrokeColor: drawing.ColorFromHex("13c158"),
StrokeWidth: 0,
}
lossStyle := chart.Style{
FillColor: drawing.ColorFromHex("c11313"),
StrokeColor: drawing.ColorFromHex("c11313"),
StrokeWidth: 0,
}
sbc := chart.BarChart{
Title: "Bar Chart Using BaseValue",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
BarWidth: 60,
YAxis: chart.YAxis{
Ticks: []chart.Tick{
{Value: -4.0, Label: "-4"},
{Value: -2.0, Label: "-2"},
{Value: 0, Label: "0"},
{Value: 2.0, Label: "2"},
{Value: 4.0, Label: "4"},
{Value: 6.0, Label: "6"},
{Value: 8.0, Label: "8"},
{Value: 10.0, Label: "10"},
{Value: 12.0, Label: "12"},
},
},
UseBaseValue: true,
BaseValue: 0.0,
Bars: []chart.Value{
{Value: 10.0, Style: profitStyle, Label: "Profit"},
{Value: 12.0, Style: profitStyle, Label: "More Profit"},
{Value: 8.0, Style: profitStyle, Label: "Still Profit"},
{Value: -4.0, Style: lossStyle, Label: "Loss!"},
{Value: 3.0, Style: profitStyle, Label: "Phew Ok"},
{Value: -2.0, Style: lossStyle, Label: "Oh No!"},
},
}
f, _ := os.Create("output.png")
defer f.Close()
sbc.Render(chart.PNG, f)
}

Some files were not shown because too many files have changed in this diff Show more