diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index b48761c..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -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 ./... diff --git a/.gitignore b/.gitignore index 8f4388f..a0f0e53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,2 @@ -# 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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fdd7948 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.8.1 + +sudo: false + +before_script: + - go get -u github.com/blendlabs/go-assert + - go get ./... + +script: + - go test ./... diff --git a/COVERAGE b/COVERAGE deleted file mode 100644 index 7e492f8..0000000 --- a/COVERAGE +++ /dev/null @@ -1 +0,0 @@ -29.02 \ No newline at end of file diff --git a/LICENSE b/LICENSE index a1ec28f..8d9f1c5 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/Makefile b/Makefile index e0928c3..cc16258 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,9 @@ -all: new-install test - -new-install: - @go get -v -u ./... - -generate: - @go generate ./... +all: test test: - @go test ./... \ No newline at end of file + @go test ./... + +cover: + @go test -short -covermode=set -coverprofile=profile.cov + @go tool cover -html=profile.cov + @rm profile.cov \ No newline at end of file diff --git a/PROFANITY_RULES.yml b/PROFANITY_RULES.yml deleted file mode 100644 index 1e6c803..0000000 --- a/PROFANITY_RULES.yml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/README.md b/README.md index 872548b..0ed0a51 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/_colors/colors_extended.txt b/_colors/colors_extended.txt deleted file mode 100644 index 65cd288..0000000 --- a/_colors/colors_extended.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/examples/annotations/main.go b/_examples/annotations/main.go similarity index 81% rename from examples/annotations/main.go rename to _examples/annotations/main.go index b9d22be..84220b1 100644 --- a/examples/annotations/main.go +++ b/_examples/annotations/main.go @@ -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) } diff --git a/_examples/annotations/output.png b/_examples/annotations/output.png new file mode 100644 index 0000000..a2e80fa Binary files /dev/null and b/_examples/annotations/output.png differ diff --git a/examples/axes_labels/main.go b/_examples/axes/main.go similarity index 63% rename from examples/axes_labels/main.go rename to _examples/axes/main.go index 6c9055d..ee0a88c 100644 --- a/examples/axes_labels/main.go +++ b/_examples/axes/main.go @@ -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) } diff --git a/_examples/axes/output.png b/_examples/axes/output.png new file mode 100644 index 0000000..cacd47d Binary files /dev/null and b/_examples/axes/output.png differ diff --git a/examples/axes/main.go b/_examples/axes_labels/main.go similarity index 57% rename from examples/axes/main.go rename to _examples/axes_labels/main.go index 7847ffa..6074259 100644 --- a/examples/axes/main.go +++ b/_examples/axes_labels/main.go @@ -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) } diff --git a/_examples/axes_labels/output.png b/_examples/axes_labels/output.png new file mode 100644 index 0000000..61c376c Binary files /dev/null and b/_examples/axes_labels/output.png differ diff --git a/examples/bar_chart_web/main.go b/_examples/bar_chart/main.go similarity index 78% rename from examples/bar_chart_web/main.go rename to _examples/bar_chart/main.go index 714276e..6afc886 100644 --- a/examples/bar_chart_web/main.go +++ b/_examples/bar_chart/main.go @@ -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 { diff --git a/_examples/bar_chart/output.png b/_examples/bar_chart/output.png new file mode 100644 index 0000000..59c6c89 Binary files /dev/null and b/_examples/bar_chart/output.png differ diff --git a/_examples/basic/main.go b/_examples/basic/main.go new file mode 100644 index 0000000..1fd9d38 --- /dev/null +++ b/_examples/basic/main.go @@ -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}, + YValues: []float64{1.0, 2.0, 3.0, 4.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)) +} diff --git a/_examples/basic/output.png b/_examples/basic/output.png new file mode 100644 index 0000000..2c781a7 Binary files /dev/null and b/_examples/basic/output.png differ diff --git a/_examples/benchmark_line_charts/main.go b/_examples/benchmark_line_charts/main.go new file mode 100644 index 0000000..09b1b89 --- /dev/null +++ b/_examples/benchmark_line_charts/main.go @@ -0,0 +1,79 @@ +// Usage: http://localhost:8080?series=100&values=1000 +package main + +import ( + "fmt" + "math/rand" + "net/http" + "strconv" + "time" + + "github.com/wcharczuk/go-chart" +) + +func random(min, max float64) float64 { + return rand.Float64()*(max-min) + min +} + +func drawLargeChart(res http.ResponseWriter, r *http.Request) { + numSeriesInt64, err := strconv.ParseInt(r.FormValue("series"), 10, 64) + if err != nil { + numSeriesInt64 = int64(1) + } + if numSeriesInt64 == 0 { + numSeriesInt64 = 1 + } + numSeries := int(numSeriesInt64) + + numValuesInt64, err := strconv.ParseInt(r.FormValue("values"), 10, 64) + if err != nil { + numValuesInt64 = int64(100) + } + if numValuesInt64 == 0 { + numValuesInt64 = int64(100) + } + numValues := int(numValuesInt64) + + series := make([]chart.Series, numSeries) + + for i := 0; i < numSeries; i++ { + xValues := make([]time.Time, numValues) + yValues := make([]float64, numValues) + + for j := 0; j < numValues; j++ { + xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1) + yValues[j] = random(float64(-500), float64(500)) + } + + series[i] = chart.TimeSeries{ + Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i), + XValues: xValues, + YValues: yValues, + } + } + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Name: "Time", + NameStyle: chart.StyleShow(), + Style: chart.StyleShow(), + }, + YAxis: chart.YAxis{ + Name: "Value", + NameStyle: chart.StyleShow(), + Style: chart.StyleShow(), + }, + Series: series, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawLargeChart) + http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) { + res.Write([]byte{}) + }) + http.ListenAndServe(":8080", nil) +} diff --git a/_examples/benchmark_line_charts/output.png b/_examples/benchmark_line_charts/output.png new file mode 100644 index 0000000..444c790 Binary files /dev/null and b/_examples/benchmark_line_charts/output.png differ diff --git a/_examples/candlestick_series/main.go b/_examples/candlestick_series/main.go new file mode 100644 index 0000000..c143029 --- /dev/null +++ b/_examples/candlestick_series/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "math/rand" + "net/http" + "time" + + chart "github.com/wcharczuk/go-chart" + util "github.com/wcharczuk/go-chart/util" +) + +func stockData() (times []time.Time, prices []float64) { + start := time.Date(2017, 05, 15, 9, 30, 0, 0, util.Date.Eastern()) + price := 256.0 + for day := 0; day < 60; day++ { + cursor := start.AddDate(0, 0, day) + + if util.Date.IsNYSEHoliday(cursor) { + continue + } + + for minute := 0; minute < ((6 * 60) + 30); minute++ { + cursor = cursor.Add(time.Minute) + + if rand.Float64() >= 0.5 { + price = price + (rand.Float64() * (price * 0.01)) + } else { + price = price - (rand.Float64() * (price * 0.01)) + } + + times = append(times, cursor) + prices = append(prices, price) + } + } + return +} + +func drawChart(res http.ResponseWriter, req *http.Request) { + xv, yv := stockData() + + priceSeries := chart.TimeSeries{ + Name: "SPY", + Style: chart.Style{ + Show: false, + StrokeColor: chart.GetDefaultColor(0), + }, + XValues: xv, + YValues: yv, + } + + candleSeries := chart.CandlestickSeries{ + Name: "SPY", + XValues: xv, + YValues: yv, + } + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Style: chart.Style{Show: true, FontSize: 8, TextRotationDegrees: 45}, + TickPosition: chart.TickPositionUnderTick, + Range: &chart.MarketHoursRange{}, + }, + YAxis: chart.YAxis{ + Style: chart.Style{Show: true}, + }, + Series: []chart.Series{ + candleSeries, + priceSeries, + }, + } + + res.Header().Set("Content-Type", "image/png") + err := graph.Render(chart.PNG, res) + if err != nil { + panic(err) + } +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/custom_formatters/main.go b/_examples/custom_formatters/main.go similarity index 70% rename from examples/custom_formatters/main.go rename to _examples/custom_formatters/main.go index fbb3317..5da4b00 100644 --- a/examples/custom_formatters/main.go +++ b/_examples/custom_formatters/main.go @@ -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) } diff --git a/_examples/custom_formatters/output.png b/_examples/custom_formatters/output.png new file mode 100644 index 0000000..ced62f1 Binary files /dev/null and b/_examples/custom_formatters/output.png differ diff --git a/_examples/custom_padding/main.go b/_examples/custom_padding/main.go new file mode 100644 index 0000000..f1a5db5 --- /dev/null +++ b/_examples/custom_padding/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/drawing" + "github.com/wcharczuk/go-chart/seq" +) + +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: seq.Range(1.0, 100.0), + YValues: seq.RandomValuesWithMax(100, 512), + }, + }, + } + + 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: seq.Range(1.0, 100.0), + YValues: seq.RandomValuesWithMax(100, 512), + }, + }, + } + + 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) +} diff --git a/_examples/custom_padding/output.png b/_examples/custom_padding/output.png new file mode 100644 index 0000000..368e539 Binary files /dev/null and b/_examples/custom_padding/output.png differ diff --git a/examples/custom_ranges/main.go b/_examples/custom_ranges/main.go similarity index 65% rename from examples/custom_ranges/main.go rename to _examples/custom_ranges/main.go index 01d3899..4529684 100644 --- a/examples/custom_ranges/main.go +++ b/_examples/custom_ranges/main.go @@ -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) } diff --git a/_examples/custom_ranges/output.png b/_examples/custom_ranges/output.png new file mode 100644 index 0000000..51b2321 Binary files /dev/null and b/_examples/custom_ranges/output.png differ diff --git a/examples/custom_styles/main.go b/_examples/custom_styles/main.go similarity index 61% rename from examples/custom_styles/main.go rename to _examples/custom_styles/main.go index 0ee519a..4c93856 100644 --- a/examples/custom_styles/main.go +++ b/_examples/custom_styles/main.go @@ -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) } diff --git a/_examples/custom_styles/output.png b/_examples/custom_styles/output.png new file mode 100644 index 0000000..84066a4 Binary files /dev/null and b/_examples/custom_styles/output.png differ diff --git a/examples/custom_ticks/main.go b/_examples/custom_ticks/main.go similarity index 60% rename from examples/custom_ticks/main.go rename to _examples/custom_ticks/main.go index 9bc5aa8..21b9d31 100644 --- a/examples/custom_ticks/main.go +++ b/_examples/custom_ticks/main.go @@ -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) } diff --git a/_examples/custom_ticks/output.png b/_examples/custom_ticks/output.png new file mode 100644 index 0000000..aefa3c3 Binary files /dev/null and b/_examples/custom_ticks/output.png differ diff --git a/examples/descending/main.go b/_examples/descending/main.go similarity index 73% rename from examples/descending/main.go rename to _examples/descending/main.go index 2d2e899..6270104 100644 --- a/examples/descending/main.go +++ b/_examples/descending/main.go @@ -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) } diff --git a/_examples/descending/output.png b/_examples/descending/output.png new file mode 100644 index 0000000..a9a00a9 Binary files /dev/null and b/_examples/descending/output.png differ diff --git a/examples/image_writer/main.go b/_examples/image_writer/main.go similarity index 92% rename from examples/image_writer/main.go rename to _examples/image_writer/main.go index ce0a145..13bf013 100644 --- a/examples/image_writer/main.go +++ b/_examples/image_writer/main.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart" ) func main() { diff --git a/examples/legend/main.go b/_examples/legend/main.go similarity index 68% rename from examples/legend/main.go rename to _examples/legend/main.go index da4c212..41cff72 100644 --- a/examples/legend/main.go +++ b/_examples/legend/main.go @@ -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) } diff --git a/_examples/legend/output.png b/_examples/legend/output.png new file mode 100644 index 0000000..d5fd6d8 Binary files /dev/null and b/_examples/legend/output.png differ diff --git a/examples/legend_left/main.go b/_examples/legend_left/main.go similarity index 86% rename from examples/legend_left/main.go rename to _examples/legend_left/main.go index 1e87578..b5e61b6 100644 --- a/examples/legend_left/main.go +++ b/_examples/legend_left/main.go @@ -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) } diff --git a/_examples/legend_left/output.png b/_examples/legend_left/output.png new file mode 100644 index 0000000..7708b48 Binary files /dev/null and b/_examples/legend_left/output.png differ diff --git a/examples/linear_regression/main.go b/_examples/linear_regression/main.go similarity index 60% rename from examples/linear_regression/main.go rename to _examples/linear_regression/main.go index 6f268f0..8209f9f 100644 --- a/examples/linear_regression/main.go +++ b/_examples/linear_regression/main.go @@ -1,14 +1,13 @@ 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" + "github.com/wcharczuk/go-chart/seq" ) -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. @@ -17,8 +16,8 @@ func main() { 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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + YValues: seq.RandomValuesWithAverage(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 +33,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) } diff --git a/_examples/linear_regression/output.png b/_examples/linear_regression/output.png new file mode 100644 index 0000000..f4c09a1 Binary files /dev/null and b/_examples/linear_regression/output.png differ diff --git a/_examples/market_hours/main.go b/_examples/market_hours/main.go new file mode 100644 index 0000000..d0be1d3 --- /dev/null +++ b/_examples/market_hours/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/seq" + "github.com/wcharczuk/go-chart/util" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + start := util.Date.Date(2016, 7, 01, util.Date.Eastern()) + end := util.Date.Date(2016, 07, 21, util.Date.Eastern()) + xv := seq.Time.MarketHours(start, end, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) + yv := seq.New(seq.NewRandom().WithLen(len(xv)).WithAverage(200).WithScale(10)).Array() + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Style: chart.StyleShow(), + TickPosition: chart.TickPositionBetweenTicks, + ValueFormatter: chart.TimeHourValueFormatter, + Range: &chart.MarketHoursRange{ + MarketOpen: util.NYSEOpen(), + MarketClose: util.NYSEClose(), + HolidayProvider: util.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) +} diff --git a/_examples/market_hours/output.png b/_examples/market_hours/output.png new file mode 100644 index 0000000..82610ef Binary files /dev/null and b/_examples/market_hours/output.png differ diff --git a/examples/min_max/main.go b/_examples/min_max/main.go similarity index 51% rename from examples/min_max/main.go rename to _examples/min_max/main.go index 78e7b1c..d8cc3a0 100644 --- a/examples/min_max/main.go +++ b/_examples/min_max/main.go @@ -1,22 +1,22 @@ package main -//go:generate go run main.go - import ( - "os" + "net/http" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/seq" ) -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: seq.Range(1.0, 100.0), + YValues: seq.New(seq.NewRandom().WithLen(100).WithAverage(100).WithScale(50)).Array(), } minSeries := &chart.MinSeries{ Style: chart.Style{ + Show: true, StrokeColor: chart.ColorAlternateGray, StrokeDashArray: []float64{5.0, 5.0}, }, @@ -25,6 +25,7 @@ func main() { maxSeries := &chart.MaxSeries{ Style: chart.Style{ + Show: true, StrokeColor: chart.ColorAlternateGray, StrokeDashArray: []float64{5.0, 5.0}, }, @@ -35,27 +36,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) } diff --git a/_examples/min_max/output.png b/_examples/min_max/output.png new file mode 100644 index 0000000..e2e1b38 Binary files /dev/null and b/_examples/min_max/output.png differ diff --git a/_examples/overlap/main.go b/_examples/overlap/main.go new file mode 100644 index 0000000..249e8cf --- /dev/null +++ b/_examples/overlap/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "net/http" + + chart "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/drawing" +) + +func conditionalColor(condition bool, trueColor drawing.Color, falseColor drawing.Color) drawing.Color { + if condition { + return trueColor + } + return falseColor +} + +func drawChart(res http.ResponseWriter, req *http.Request) { + r, _ := chart.PNG(1024, 1024) + + b0 := chart.Box{Left: 100, Top: 100, Right: 400, Bottom: 200} + b1 := chart.Box{Left: 500, Top: 100, Right: 900, Bottom: 200} + b0r := b0.Corners().Rotate(45).Shift(0, 200) + + chart.Draw.Box(r, b0, chart.Style{ + StrokeColor: drawing.ColorRed, + StrokeWidth: 2, + FillColor: conditionalColor(b0.Corners().Overlaps(b1.Corners()), drawing.ColorRed, drawing.ColorTransparent), + }) + + chart.Draw.Box(r, b1, chart.Style{ + StrokeColor: drawing.ColorBlue, + StrokeWidth: 2, + FillColor: conditionalColor(b1.Corners().Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent), + }) + + chart.Draw.Box2d(r, b0r, chart.Style{ + StrokeColor: drawing.ColorGreen, + StrokeWidth: 2, + FillColor: conditionalColor(b0r.Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent), + }) + + res.Header().Set("Content-Type", "image/png") + r.Save(res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/pie_chart_web/main.go b/_examples/pie_chart/main.go similarity index 57% rename from examples/pie_chart_web/main.go rename to _examples/pie_chart/main.go index fa25594..d928c41 100644 --- a/examples/pie_chart_web/main.go +++ b/_examples/pie_chart/main.go @@ -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)) } diff --git a/examples/pie_chart/output.png b/_examples/pie_chart/output.png similarity index 100% rename from examples/pie_chart/output.png rename to _examples/pie_chart/output.png diff --git a/examples/poly_regression/main.go b/_examples/poly_regression/main.go similarity index 51% rename from examples/poly_regression/main.go rename to _examples/poly_regression/main.go index 9f797d3..9ca6b2b 100644 --- a/examples/poly_regression/main.go +++ b/_examples/poly_regression/main.go @@ -1,14 +1,13 @@ 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" + "github.com/wcharczuk/go-chart/seq" ) -func main() { +func drawChart(res http.ResponseWriter, req *http.Request) { /* In this example we add a new type of series, a `PolynomialRegressionSeries` that takes another series as a required argument. @@ -17,8 +16,8 @@ func main() { 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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + YValues: seq.RandomValuesWithAverage(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. } polyRegSeries := &chart.PolynomialRegressionSeries{ @@ -33,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) } diff --git a/_examples/poly_regression/output.png b/_examples/poly_regression/output.png new file mode 100644 index 0000000..ae9b8f6 Binary files /dev/null and b/_examples/poly_regression/output.png differ diff --git a/_examples/request_timings/main.go b/_examples/request_timings/main.go new file mode 100644 index 0000000..d5867af --- /dev/null +++ b/_examples/request_timings/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/wcharczuk/go-chart" + util "github.com/wcharczuk/go-chart/util" +) + +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 := util.File.ReadByLines("requests.csv", func(line string) error { + 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) + return nil + }) + if err != nil { + fmt.Println(err.Error()) + } + return xvalues, yvalues +} + +func releases() []chart.GridLine { + return []chart.GridLine{ + {Value: util.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))}, + {Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))}, + {Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))}, + {Value: util.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))}, + {Value: util.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))}, + {Value: util.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", chart.ContentTypePNG) + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/request_timings/output.png b/_examples/request_timings/output.png similarity index 100% rename from examples/request_timings/output.png rename to _examples/request_timings/output.png diff --git a/examples/request_timings/requests.csv b/_examples/request_timings/requests.csv similarity index 100% rename from examples/request_timings/requests.csv rename to _examples/request_timings/requests.csv diff --git a/examples/scatter/main.go b/_examples/scatter/main.go similarity index 68% rename from examples/scatter/main.go rename to _examples/scatter/main.go index c861104..6d0560c 100644 --- a/examples/scatter/main.go +++ b/_examples/scatter/main.go @@ -6,8 +6,9 @@ import ( _ "net/http/pprof" - "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" + "github.com/wcharczuk/go-chart/seq" ) func drawChart(res http.ResponseWriter, req *http.Request) { @@ -20,12 +21,13 @@ func drawChart(res http.ResponseWriter, req *http.Request) { Series: []chart.Series{ chart.ContinuousSeries{ Style: chart.Style{ + Show: true, StrokeWidth: chart.Disabled, DotWidth: 5, DotColorProvider: viridisByY, }, - XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(127)}.Values(), - YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(128).WithMin(0).WithMax(1024)}.Values(), + XValues: seq.Range(0, 127), + YValues: seq.New(seq.NewRandom().WithLen(128).WithMax(1024)).Array(), }, }, } @@ -42,15 +44,15 @@ func unit(res http.ResponseWriter, req *http.Request) { Height: 50, Width: 50, Canvas: chart.Style{ - Padding: chart.BoxZero, + Padding: chart.Box{IsSet: true}, }, Background: chart.Style{ - Padding: chart.BoxZero, + Padding: chart.Box{IsSet: true}, }, Series: []chart.Series{ chart.ContinuousSeries{ - XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), - YValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), + XValues: seq.RangeWithStep(0, 4, 1), + YValues: seq.RangeWithStep(0, 4, 1), }, }, } diff --git a/examples/scatter/output.png b/_examples/scatter/output.png similarity index 100% rename from examples/scatter/output.png rename to _examples/scatter/output.png diff --git a/examples/simple_moving_average/main.go b/_examples/simple_moving_average/main.go similarity index 50% rename from examples/simple_moving_average/main.go rename to _examples/simple_moving_average/main.go index 2d7a03c..3020b0a 100644 --- a/examples/simple_moving_average/main.go +++ b/_examples/simple_moving_average/main.go @@ -1,18 +1,18 @@ package main -//go:generate go run main.go - import ( - "os" + "net/http" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/seq" ) -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(), //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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. } // note we create a SimpleMovingAverage series by assignin the inner series. @@ -28,7 +28,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) } diff --git a/_examples/simple_moving_average/output.png b/_examples/simple_moving_average/output.png new file mode 100644 index 0000000..8671d97 Binary files /dev/null and b/_examples/simple_moving_average/output.png differ diff --git a/examples/stacked_bar/main.go b/_examples/stacked_bar/main.go similarity index 64% rename from examples/stacked_bar/main.go rename to _examples/stacked_bar/main.go index 234ada3..07304bc 100644 --- a/examples/stacked_bar/main.go +++ b/_examples/stacked_bar/main.go @@ -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)) } diff --git a/examples/stacked_bar/output.png b/_examples/stacked_bar/output.png similarity index 100% rename from examples/stacked_bar/output.png rename to _examples/stacked_bar/output.png diff --git a/examples/stock_analysis/main.go b/_examples/stock_analysis/main.go similarity index 93% rename from examples/stock_analysis/main.go rename to _examples/stock_analysis/main.go index 4640a59..ddfa4b3 100644 --- a/examples/stock_analysis/main.go +++ b/_examples/stock_analysis/main.go @@ -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) +} diff --git a/_examples/stock_analysis/output.png b/_examples/stock_analysis/output.png new file mode 100644 index 0000000..bf67ba3 Binary files /dev/null and b/_examples/stock_analysis/output.png differ diff --git a/examples/text_rotation/main.go b/_examples/text_rotation/main.go similarity index 62% rename from examples/text_rotation/main.go rename to _examples/text_rotation/main.go index da41c50..53ca040 100644 --- a/examples/text_rotation/main.go +++ b/_examples/text_rotation/main.go @@ -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) @@ -34,18 +32,28 @@ func main() { tbc := tb.Corners().Rotate(45) - chart.Draw.BoxCorners(r, tbc, chart.Style{ + chart.Draw.Box2d(r, tbc, chart.Style{ StrokeColor: drawing.ColorRed, StrokeWidth: 2, }) + tbc2 := tbc.Shift(tbc.Height(), 0) + chart.Draw.Box2d(r, tbc2, chart.Style{ + StrokeColor: drawing.ColorGreen, + StrokeWidth: 2, + }) + tbcb := tbc.Box() chart.Draw.Box(r, tbcb, chart.Style{ StrokeColor: drawing.ColorBlue, 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) } diff --git a/examples/text_rotation/output.png b/_examples/text_rotation/output.png similarity index 100% rename from examples/text_rotation/output.png rename to _examples/text_rotation/output.png diff --git a/examples/timeseries/main.go b/_examples/timeseries/main.go similarity index 86% rename from examples/timeseries/main.go rename to _examples/timeseries/main.go index 61a8fc2..9303ab7 100644 --- a/examples/timeseries/main.go +++ b/_examples/timeseries/main.go @@ -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) diff --git a/examples/timeseries/output.png b/_examples/timeseries/output.png similarity index 100% rename from examples/timeseries/output.png rename to _examples/timeseries/output.png diff --git a/examples/twoaxis/main.go b/_examples/twoaxis/main.go similarity index 57% rename from examples/twoaxis/main.go rename to _examples/twoaxis/main.go index 62fce18..baac87e 100644 --- a/examples/twoaxis/main.go +++ b/_examples/twoaxis/main.go @@ -1,15 +1,14 @@ 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" + util "github.com/wcharczuk/go-chart/util" ) -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 +18,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 := util.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 +51,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) } diff --git a/examples/twoaxis/output.png b/_examples/twoaxis/output.png similarity index 100% rename from examples/twoaxis/output.png rename to _examples/twoaxis/output.png diff --git a/examples/twopoint/main.go b/_examples/twopoint/main.go similarity index 78% rename from examples/twopoint/main.go rename to _examples/twopoint/main.go index 26a4fa1..d51d37e 100644 --- a/examples/twopoint/main.go +++ b/_examples/twopoint/main.go @@ -1,27 +1,30 @@ package main -//go:generate go run main.go - import ( "bytes" "log" "os" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart" ) func main() { + var b float64 b = 1000 ts1 := chart.ContinuousSeries{ //TimeSeries{ - Name: "Time Series", + Name: "Time Series", + Style: chart.Style{ + Show: true, + }, 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 +36,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{ diff --git a/_examples/twopoint/output.png b/_examples/twopoint/output.png new file mode 100644 index 0000000..8d35b97 Binary files /dev/null and b/_examples/twopoint/output.png differ diff --git a/annotation_series.go b/annotation_series.go index 96e78f9..9c383c9 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -3,11 +3,8 @@ package chart import ( "fmt" "math" -) -// Interface Assertions. -var ( - _ Series = (*AnnotationSeries)(nil) + util "github.com/wcharczuk/go-chart/util" ) // AnnotationSeries is a series of labels on the chart. @@ -53,17 +50,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 = util.Math.MinInt(box.Top, ab.Top) + box.Left = util.Math.MinInt(box.Left, ab.Left) + box.Right = util.Math.MaxInt(box.Right, ab.Right) + box.Bottom = util.Math.MaxInt(box.Bottom, ab.Bottom) } } return box @@ -71,7 +68,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) diff --git a/annotation_series_test.go b/annotation_series_test.go index 251e8f4..8945836 100644 --- a/annotation_series_test.go +++ b/annotation_series_test.go @@ -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) } diff --git a/array.go b/array.go deleted file mode 100644 index 71b3ee7..0000000 --- a/array.go +++ /dev/null @@ -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] -} diff --git a/bar_chart.go b/bar_chart.go index ce22ba7..1269678 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -7,6 +7,7 @@ import ( "math" "github.com/golang/freetype/truetype" + util "github.com/wcharczuk/go-chart/util" ) // BarChart is a chart that draws bars on a range. @@ -30,9 +31,6 @@ type BarChart struct { BarSpacing int - UseBaseValue bool - BaseValue float64 - Font *truetype.Font defaultFont *truetype.Font @@ -128,7 +126,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 +139,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 +195,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 +209,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 +248,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 Box2d + 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+(int(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 +305,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 +351,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 +369,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 = util.Math.MinInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight) } } @@ -398,7 +383,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 +397,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, } @@ -451,7 +436,7 @@ func (bc BarChart) styleDefaultsTitle() Style { } func (bc BarChart) getTitleFontSize() float64 { - effectiveDimension := MinInt(bc.GetWidth(), bc.GetHeight()) + effectiveDimension := util.Math.MinInt(bc.GetWidth(), bc.GetHeight()) if effectiveDimension >= 2048 { return 48 } else if effectiveDimension >= 1024 { diff --git a/bar_chart_test.go b/bar_chart_test.go index 7bd98ee..2a8e96d 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -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) } diff --git a/bollinger_band_series.go b/bollinger_band_series.go index 728b232..9dbd3b8 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -2,11 +2,8 @@ package chart import ( "fmt" -) -// Interface Assertions. -var ( - _ Series = (*BollingerBandsSeries)(nil) + "github.com/wcharczuk/go-chart/seq" ) // BollingerBandsSeries draws bollinger bands for an inner series. @@ -20,7 +17,7 @@ type BollingerBandsSeries struct { K float64 InnerSeries ValuesProvider - valueBuffer *ValueBuffer + valueBuffer *seq.Buffer } // GetName returns the name of the time series. @@ -70,7 +67,7 @@ func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) return } if bbs.valueBuffer == nil || index == 0 { - bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod()) + bbs.valueBuffer = seq.NewBufferWithCapacity(bbs.GetPeriod()) } if bbs.valueBuffer.Len() >= bbs.GetPeriod() { bbs.valueBuffer.Dequeue() @@ -79,8 +76,8 @@ func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) bbs.valueBuffer.Enqueue(py) x = px - ay := Seq{bbs.valueBuffer}.Average() - std := Seq{bbs.valueBuffer}.StdDev() + ay := seq.New(bbs.valueBuffer).Average() + std := seq.New(bbs.valueBuffer).StdDev() y1 = ay + (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std) @@ -99,15 +96,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) { startAt = 0 } - vb := NewValueBufferWithCapacity(period) + vb := seq.NewBufferWithCapacity(period) for index := startAt; index < seriesLength; index++ { xn, yn := bbs.InnerSeries.GetValues(index) vb.Enqueue(yn) x = xn } - ay := Seq{vb}.Average() - std := Seq{vb}.StdDev() + ay := seq.Seq{Provider: vb}.Average() + std := seq.Seq{Provider: vb}.StdDev() y1 = ay + (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std) diff --git a/bollinger_band_series_test.go b/bollinger_band_series_test.go index e281e2d..9ef75fe 100644 --- a/bollinger_band_series_test.go +++ b/bollinger_band_series_test.go @@ -5,15 +5,16 @@ import ( "math" "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) 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), + X: seq.Range(1.0, 100.0), + Y: seq.RandomValuesWithMax(100, 1024), } bbs := &BollingerBandsSeries{ @@ -29,16 +30,16 @@ func TestBollingerBandSeries(t *testing.T) { } 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], fmt.Sprintf("%v vs. %v", 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), + X: seq.Range(1.0, 100.0), + Y: seq.Range(1.0, 100.0), } bbs := &BollingerBandsSeries{ @@ -46,7 +47,7 @@ func TestBollingerBandLastValue(t *testing.T) { } 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)) + assert.Equal(100.0, x) + assert.Equal(101, math.Floor(y1)) + assert.Equal(83, math.Floor(y2)) } diff --git a/bounded_last_values_annotation_series.go b/bounded_last_values_annotation_series.go deleted file mode 100644 index 670ddf7..0000000 --- a/bounded_last_values_annotation_series.go +++ /dev/null @@ -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}, - }, - } -} diff --git a/box.go b/box.go index 2641bd4..c6c0baa 100644 --- a/box.go +++ b/box.go @@ -2,7 +2,8 @@ package chart import ( "fmt" - "math" + + util "github.com/wcharczuk/go-chart/util" ) var ( @@ -89,12 +90,12 @@ func (b Box) GetBottom(defaults ...int) int { // Width returns the width func (b Box) Width() int { - return AbsInt(b.Right - b.Left) + return util.Math.AbsInt(b.Right - b.Left) } // Height returns the height func (b Box) Height() int { - return AbsInt(b.Bottom - b.Top) + return util.Math.AbsInt(b.Bottom - b.Top) } // Center returns the center of the box @@ -146,10 +147,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: util.Math.MinInt(b.Top, other.Top), + Left: util.Math.MinInt(b.Left, other.Left), + Right: util.Math.MaxInt(b.Right, other.Right), + Bottom: util.Math.MaxInt(b.Bottom, other.Bottom), } } @@ -164,12 +165,12 @@ func (b Box) Shift(x, y int) Box { } // Corners returns the box as a set of corners. -func (b Box) Corners() BoxCorners { - return BoxCorners{ - TopLeft: Point{b.Left, b.Top}, - TopRight: Point{b.Right, b.Top}, - BottomRight: Point{b.Right, b.Bottom}, - BottomLeft: Point{b.Left, b.Bottom}, +func (b Box) Corners() Box2d { + return Box2d{ + TopLeft: Point{float64(b.Left), float64(b.Top)}, + TopRight: Point{float64(b.Right), float64(b.Top)}, + BottomRight: Point{float64(b.Right), float64(b.Bottom)}, + BottomLeft: Point{float64(b.Left), float64(b.Bottom)}, } } @@ -220,10 +221,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 = util.Math.MaxInt(newBox.Top, other.Top) + newBox.Left = util.Math.MaxInt(newBox.Left, other.Left) + newBox.Right = util.Math.MinInt(newBox.Right, other.Right) + newBox.Bottom = util.Math.MinInt(newBox.Bottom, other.Bottom) return newBox } @@ -253,115 +254,3 @@ 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 -} - -// 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), - } -} - -// 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) - 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) - 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) - x = ((right - left) >> 1) + left - - top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y) - bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y) - y = ((bottom - top) >> 1) + top - - return -} - -// Rotate rotates the box. -func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners { - cx, cy := bc.Center() - - thetaRadians := 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) - - return BoxCorners{ - TopLeft: Point{tlx, tly}, - TopRight: Point{trx, try}, - BottomRight: Point{brx, bry}, - BottomLeft: Point{blx, bly}, - } -} - -// Equals returns if the box equals another box. -func (bc BoxCorners) Equals(other BoxCorners) bool { - return bc.TopLeft.Equals(other.TopLeft) && - bc.TopRight.Equals(other.TopRight) && - bc.BottomRight.Equals(other.BottomRight) && - bc.BottomLeft.Equals(other.BottomLeft) -} - -func (bc BoxCorners) String() string { - return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String()) -} - -// Point is an X,Y pair -type Point struct { - X, Y int -} - -// DistanceTo calculates the distance to another point. -func (p Point) DistanceTo(other Point) float64 { - dx := math.Pow(float64(p.X-other.X), 2) - dy := math.Pow(float64(p.Y-other.Y), 2) - return math.Pow(dx+dy, 0.5) -} - -// Equals returns if a point equals another point. -func (p Point) Equals(other Point) bool { - return p.X == other.X && p.Y == other.Y -} - -// String returns a string representation of the point. -func (p Point) String() string { - return fmt.Sprintf("P{%d,%d}", p.X, p.Y) -} diff --git a/box_2d.go b/box_2d.go new file mode 100644 index 0000000..04759d6 --- /dev/null +++ b/box_2d.go @@ -0,0 +1,183 @@ +package chart + +import ( + "fmt" + "math" + + util "github.com/wcharczuk/go-chart/util" +) + +// Box2d is a box with (4) independent corners. +// It is used when dealing with ~rotated~ boxes. +type Box2d struct { + TopLeft, TopRight, BottomRight, BottomLeft Point +} + +// Points returns the constituent points of the box. +func (bc Box2d) Points() []Point { + return []Point{ + bc.TopRight, + bc.BottomRight, + bc.BottomLeft, + bc.TopLeft, + } +} + +// Box return the Box2d as a regular box. +func (bc Box2d) Box() Box { + return Box{ + Top: int(bc.Top()), + Left: int(bc.Left()), + Right: int(bc.Right()), + Bottom: int(bc.Bottom()), + } +} + +// Top returns the top-most corner y value. +func (bc Box2d) Top() float64 { + return math.Min(bc.TopLeft.Y, bc.TopRight.Y) +} + +// Left returns the left-most corner x value. +func (bc Box2d) Left() float64 { + return math.Min(bc.TopLeft.X, bc.BottomLeft.X) +} + +// Right returns the right-most corner x value. +func (bc Box2d) Right() float64 { + return math.Max(bc.TopRight.X, bc.BottomRight.X) +} + +// Bottom returns the bottom-most corner y value. +func (bc Box2d) Bottom() float64 { + return math.Max(bc.BottomLeft.Y, bc.BottomLeft.Y) +} + +// Width returns the width +func (bc Box2d) Width() float64 { + minLeft := math.Min(bc.TopLeft.X, bc.BottomLeft.X) + maxRight := math.Max(bc.TopRight.X, bc.BottomRight.X) + return maxRight - minLeft +} + +// Height returns the height +func (bc Box2d) Height() float64 { + minTop := math.Min(bc.TopLeft.Y, bc.TopRight.Y) + maxBottom := math.Max(bc.BottomLeft.Y, bc.BottomRight.Y) + return maxBottom - minTop +} + +// Center returns the center of the box +func (bc Box2d) Center() (x, y float64) { + left := util.Math.Mean(bc.TopLeft.X, bc.BottomLeft.X) + right := util.Math.Mean(bc.TopRight.X, bc.BottomRight.X) + x = ((right - left) / 2.0) + left + + top := util.Math.Mean(bc.TopLeft.Y, bc.TopRight.Y) + bottom := util.Math.Mean(bc.BottomLeft.Y, bc.BottomRight.Y) + y = ((bottom - top) / 2.0) + top + + return +} + +// Rotate rotates the box. +func (bc Box2d) Rotate(thetaDegrees float64) Box2d { + cx, cy := bc.Center() + + thetaRadians := util.Math.DegreesToRadians(thetaDegrees) + + tlx, tly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopLeft.X), int(bc.TopLeft.Y), thetaRadians) + trx, try := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopRight.X), int(bc.TopRight.Y), thetaRadians) + brx, bry := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomRight.X), int(bc.BottomRight.Y), thetaRadians) + blx, bly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomLeft.X), int(bc.BottomLeft.Y), thetaRadians) + + return Box2d{ + TopLeft: Point{float64(tlx), float64(tly)}, + TopRight: Point{float64(trx), float64(try)}, + BottomRight: Point{float64(brx), float64(bry)}, + BottomLeft: Point{float64(blx), float64(bly)}, + } +} + +// Shift shifts a box by a given x and y value. +func (bc Box2d) Shift(x, y float64) Box2d { + return Box2d{ + TopLeft: bc.TopLeft.Shift(x, y), + TopRight: bc.TopRight.Shift(x, y), + BottomRight: bc.BottomRight.Shift(x, y), + BottomLeft: bc.BottomLeft.Shift(x, y), + } +} + +// Equals returns if the box equals another box. +func (bc Box2d) Equals(other Box2d) bool { + return bc.TopLeft.Equals(other.TopLeft) && + bc.TopRight.Equals(other.TopRight) && + bc.BottomRight.Equals(other.BottomRight) && + bc.BottomLeft.Equals(other.BottomLeft) +} + +// Overlaps returns if two boxes overlap. +func (bc Box2d) Overlaps(other Box2d) bool { + pa := bc.Points() + pb := other.Points() + for i := 0; i < 4; i++ { + for j := 0; j < 4; j++ { + pa0 := pa[i] + pa1 := pa[(i+1)%4] + + pb0 := pb[j] + pb1 := pb[(j+1)%4] + + if util.Math.LinesIntersect(pa0.X, pa0.Y, pa1.X, pa1.Y, pb0.X, pb0.Y, pb1.X, pb1.Y) { + return true + } + } + } + return false +} + +// Grow grows a box by a given set of dimensions. +func (bc Box2d) Grow(by Box) Box2d { + top, left, right, bottom := float64(by.Top), float64(by.Left), float64(by.Right), float64(by.Bottom) + return Box2d{ + TopLeft: Point{X: bc.TopLeft.X - left, Y: bc.TopLeft.Y - top}, + TopRight: Point{X: bc.TopRight.X + right, Y: bc.TopRight.Y - top}, + BottomRight: Point{X: bc.BottomRight.X + right, Y: bc.BottomRight.Y + bottom}, + BottomLeft: Point{X: bc.BottomLeft.X - left, Y: bc.BottomLeft.Y + bottom}, + } +} + +func (bc Box2d) String() string { + return fmt.Sprintf("Box2d{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String()) +} + +// Point is an X,Y pair +type Point struct { + X, Y float64 +} + +// Shift shifts a point. +func (p Point) Shift(x, y float64) Point { + return Point{ + X: p.X + x, + Y: p.Y + y, + } +} + +// DistanceTo calculates the distance to another point. +func (p Point) DistanceTo(other Point) float64 { + dx := math.Pow(p.X-other.X, 2) + dy := math.Pow(p.Y-other.Y, 2) + return math.Pow(dx+dy, 0.5) +} + +// Equals returns if a point equals another point. +func (p Point) Equals(other Point) bool { + return p.X == other.X && p.Y == other.Y +} + +// String returns a string representation of the point. +func (p Point) String() string { + return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y) +} diff --git a/box_2d_test.go b/box_2d_test.go new file mode 100644 index 0000000..7cb541a --- /dev/null +++ b/box_2d_test.go @@ -0,0 +1,66 @@ +package chart + +import ( + "fmt" + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestBox2dCenter(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + cx, cy := bc.Center() + assert.Equal(10, cx) + assert.Equal(10, cy) +} + +func TestBox2dRotate(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + rotated := bc.Rotate(45) + assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String()) +} + +func TestBox2dOverlaps(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + // shift meaningfully the full width of bc right. + bc2 := bc.Shift(bc.Width()+1, 0) + assert.False(bc.Overlaps(bc2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc2)) + + // shift meaningfully the full height of bc down. + bc3 := bc.Shift(0, bc.Height()+1) + assert.False(bc.Overlaps(bc3), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc3)) + + bc4 := bc.Shift(5, 0) + assert.True(bc.Overlaps(bc4)) + + bc5 := bc.Shift(0, 5) + assert.True(bc.Overlaps(bc5)) + + bcr := bc.Rotate(45) + bcr2 := bc.Rotate(45).Shift(bc.Width()/2.0, 0) + assert.True(bcr.Overlaps(bcr2), fmt.Sprintf("%v\n\t\tshould overlap\n\t%v", bcr, bcr2)) +} diff --git a/box_test.go b/box_test.go index 1670256..4c0b18a 100644 --- a/box_test.go +++ b/box_test.go @@ -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,35 +154,6 @@ func TestBoxCenter(t *testing.T) { Bottom: 30, } cx, cy := b.Center() - testutil.AssertEqual(t, 15, cx) - testutil.AssertEqual(t, 20, cy) -} - -func TestBoxCornersCenter(t *testing.T) { - // replaced new assertions helper - - bc := BoxCorners{ - TopLeft: Point{5, 5}, - TopRight: Point{15, 5}, - BottomRight: Point{15, 15}, - BottomLeft: Point{5, 15}, - } - - cx, cy := bc.Center() - testutil.AssertEqual(t, 10, cx) - testutil.AssertEqual(t, 10, cy) -} - -func TestBoxCornersRotate(t *testing.T) { - // replaced new assertions helper - - bc := BoxCorners{ - TopLeft: Point{5, 5}, - TopRight: Point{15, 5}, - BottomRight: Point{15, 15}, - BottomLeft: Point{5, 15}, - } - - rotated := bc.Rotate(45) - testutil.AssertTrue(t, rotated.TopLeft.Equals(Point{10, 3}), rotated.String()) + assert.Equal(15, cx) + assert.Equal(20, cy) } diff --git a/candlestick_series.go b/candlestick_series.go new file mode 100644 index 0000000..83b5d80 --- /dev/null +++ b/candlestick_series.go @@ -0,0 +1,157 @@ +package chart + +import ( + "fmt" + "time" + + "math" + + "github.com/wcharczuk/go-chart/util" +) + +// CandleValue is a day's data for a candlestick plot. +type CandleValue struct { + Timestamp time.Time + High float64 + Low float64 + Open float64 + Close float64 +} + +// String returns a string value for the candle value. +func (cv CandleValue) String() string { + return fmt.Sprintf("candle %s high: %.2f low: %.2f open: %.2f close: %.2f", cv.Timestamp.Format("2006-01-02"), cv.High, cv.Low, cv.Open, cv.Close) +} + +// IsZero returns if the value is zero or not. +func (cv CandleValue) IsZero() bool { + return cv.Timestamp.IsZero() +} + +// CandlestickSeries is a special type of series that takes a norma value provider +// and maps it to day value stats (high, low, open, close). +type CandlestickSeries struct { + Name string + Style Style + YAxis YAxisType + + // CandleValues will be used in place of creating them from the `InnerSeries`. + CandleValues []CandleValue + + // InnerSeries is used if the `CandleValues` are not set. + InnerSeries ValuesProvider +} + +// GetName implements Series.GetName. +func (cs *CandlestickSeries) GetName() string { + return cs.Name +} + +// GetStyle implements Series.GetStyle. +func (cs *CandlestickSeries) GetStyle() Style { + return cs.Style +} + +// GetYAxis returns which yaxis the series is mapped to. +func (cs *CandlestickSeries) GetYAxis() YAxisType { + return cs.YAxis +} + +// Len returns the length of the series. +func (cs *CandlestickSeries) Len() int { + return len(cs.GetCandleValues()) +} + +// GetBoundedValues returns the bounded values at a given index. +func (cs *CandlestickSeries) GetBoundedValues(index int) (x, y0, y1 float64) { + value := cs.GetCandleValues()[index] + return util.Time.ToFloat64(value.Timestamp), value.Low, value.High +} + +// GetCandleValues returns the candle values. +func (cs CandlestickSeries) GetCandleValues() []CandleValue { + if cs.CandleValues == nil { + cs.CandleValues = cs.GenerateCandleValues() + } + return cs.CandleValues +} + +// GenerateCandleValues returns the candlestick values for each day represented by the inner series. +func (cs CandlestickSeries) GenerateCandleValues() []CandleValue { + if cs.InnerSeries == nil { + return nil + } + + totalValues := cs.InnerSeries.Len() + if totalValues == 0 { + return nil + } + + var values []CandleValue + var lastYear, lastMonth, lastDay int + var year, month, day int + + var t time.Time + var tv, lv, v float64 + + tv, v = cs.InnerSeries.GetValues(0) + t = util.Time.FromFloat64(tv) + year, month, day = t.Year(), int(t.Month()), t.Day() + + lastYear, lastMonth, lastDay = year, month, day + + value := CandleValue{ + Timestamp: cs.newTimestamp(year, month, day), + Open: v, + Low: v, + High: v, + } + lv = v + + for i := 1; i < totalValues; i++ { + tv, v = cs.InnerSeries.GetValues(i) + t = util.Time.FromFloat64(tv) + year, month, day = t.Year(), int(t.Month()), t.Day() + + // if we've transitioned to a new day or we're on the last value + if lastYear != year || lastMonth != month || lastDay != day || i == (totalValues-1) { + value.Close = lv + values = append(values, value) + + value = CandleValue{ + Timestamp: cs.newTimestamp(year, month, day), + Open: v, + High: v, + Low: v, + } + + lastYear = year + lastMonth = month + lastDay = day + } else { + value.Low = math.Min(value.Low, v) + value.High = math.Max(value.High, v) + } + lv = v + } + + return values +} + +func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time { + return time.Date(year, time.Month(month), day, 12, 0, 0, 0, util.Date.Eastern()) +} + +// Render implements Series.Render. +func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + style := cs.Style.InheritFrom(defaults) + Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs) +} + +// Validate validates the series. +func (cs CandlestickSeries) Validate() error { + if cs.CandleValues == nil && cs.InnerSeries == nil { + return fmt.Errorf("candlestick series requires either `CandleValues` or `InnerSeries` to be set") + } + return nil +} diff --git a/candlestick_series_test.go b/candlestick_series_test.go new file mode 100644 index 0000000..c9ac536 --- /dev/null +++ b/candlestick_series_test.go @@ -0,0 +1,52 @@ +package chart + +import ( + "math/rand" + "testing" + "time" + + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" +) + +func generateDummyStockData() (times []time.Time, prices []float64) { + start := util.Date.On(util.NYSEOpen(), time.Date(2017, 05, 15, 0, 0, 0, 0, util.Date.Eastern())) + cursor := start + for day := 0; day < 60; day++ { + + if util.Date.IsWeekendDay(cursor.Weekday()) { + cursor = start.AddDate(0, 0, day) + continue + } + + for hour := 0; hour < 7; hour++ { + for minute := 0; minute < 60; minute++ { + times = append(times, cursor) + prices = append(prices, rand.Float64()*256) + cursor = cursor.Add(time.Minute) + } + + cursor = cursor.Add(time.Hour) + } + + cursor = start.AddDate(0, 0, day) + } + + return +} + +func TestCandlestickSeriesCandleValues(t *testing.T) { + assert := assert.New(t) + + xdata, ydata := generateDummyStockData() + + candleSeries := &CandlestickSeries{ + InnerSeries: TimeSeries{ + XValues: xdata, + YValues: ydata, + }, + } + + values := candleSeries.GetCandleValues() + assert.Len(values, 43) // should be 60 days per the generator. +} diff --git a/chart.go b/chart.go index 5212a43..c81f4a3 100644 --- a/chart.go +++ b/chart.go @@ -7,6 +7,7 @@ import ( "math" "github.com/golang/freetype/truetype" + util "github.com/wcharczuk/go-chart/util" ) // Chart is what we're drawing. @@ -32,8 +33,6 @@ type Chart struct { Series []Series Elements []Renderable - - Log Logger } // GetDPI returns the dpi for the chart. @@ -76,8 +75,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 @@ -103,8 +102,6 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error { 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) @@ -118,8 +115,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 +125,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 +143,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,7 +176,7 @@ 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 { seriesLength := bvp.Len() @@ -268,10 +263,11 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { yrange.SetMin(miny) yrange.SetMax(maxy) - if !c.YAxis.Style.Hidden { + // only round if we're showing the axis + if c.YAxis.Style.Show { delta := yrange.GetDelta() - roundTo := GetRoundToForDelta(delta) - rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo) + roundTo := util.Math.GetRoundToForDelta(delta) + rmin, rmax := util.Math.RoundDown(yrange.GetMin(), roundTo), util.Math.RoundUp(yrange.GetMax(), roundTo) yrange.SetMin(rmin) yrange.SetMax(rmax) @@ -290,10 +286,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { yrangeAlt.SetMin(minya) yrangeAlt.SetMax(maxya) - if !c.YAxisSecondary.Style.Hidden { + if c.YAxisSecondary.Style.Show { delta := yrangeAlt.GetDelta() - roundTo := GetRoundToForDelta(delta) - rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo) + roundTo := util.Math.GetRoundToForDelta(delta) + rmin, rmax := util.Math.RoundDown(yrangeAlt.GetMin(), roundTo), util.Math.RoundUp(yrangeAlt.GetMax(), roundTo) yrangeAlt.SetMin(rmin) yrangeAlt.SetMax(rmax) } @@ -303,7 +299,6 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { } 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 +310,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") @@ -323,9 +317,11 @@ func (c Chart) checkRanges(xr, yr, yra Range) error { if math.IsNaN(yDelta) { return errors.New("nan y-range delta") } + if yDelta == 0 { + return errors.New("zero y-range delta") + } 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") @@ -333,6 +329,9 @@ func (c Chart) checkRanges(xr, yr, yra Range) error { if math.IsNaN(yraDelta) { return errors.New("nan secondary y-range delta") } + if yraDelta == 0 { + return errors.New("zero secondary y-range delta") + } } return nil @@ -368,17 +367,17 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) { } 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 +385,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 +411,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 +432,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 +469,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,7 +491,7 @@ 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())) titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize) @@ -506,8 +502,8 @@ func (c Chart) drawTitle(r Renderer) { textWidth := textBox.Width() textHeight := textBox.Height() - titleX := (c.GetWidth() >> 1) - (textWidth >> 1) - titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight + titleX := (int(c.GetWidth()) >> 1) - (int(textWidth) >> 1) + titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(textHeight) r.Text(c.Title, titleX, titleY) } diff --git a/chart_test.go b/chart_test.go index 8db42fd..90a3c19 100644 --- a/chart_test.go +++ b/chart_test.go @@ -8,57 +8,58 @@ 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" + "github.com/wcharczuk/go-chart/seq" ) 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 +80,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 +117,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 +156,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 +182,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 +199,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 +212,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 +237,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 +266,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 +361,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 +377,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 +392,43 @@ func TestChartRegressionBadRangesByUser(t *testing.T) { }, Series: []Series{ ContinuousSeries{ - XValues: LinearRange(1.0, 10.0), - YValues: LinearRange(1.0, 10.0), + XValues: seq.Range(1.0, 10.0), + YValues: seq.Range(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: seq.Range(1.0, 10.0), + YValues: seq.Range(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: seq.Range(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 +440,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 +484,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 +492,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: seq.RangeWithStep(0, 4, 1), + YValues: seq.RangeWithStep(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: seq.RangeWithStep(0, 4, 1), + YValues: seq.RangeWithStep(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)) } diff --git a/cmd/chart/main.go b/cmd/chart/main.go deleted file mode 100644 index fa41bbe..0000000 --- a/cmd/chart/main.go +++ /dev/null @@ -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) -} diff --git a/colors.go b/colors.go index de542d8..87dd4f0 100644 --- a/colors.go +++ b/colors.go @@ -1,6 +1,6 @@ package chart -import "git.smarteching.com/zeni/go-chart/v2/drawing" +import "github.com/wcharczuk/go-chart/drawing" var ( // ColorWhite is white. diff --git a/concat_series_test.go b/concat_series_test.go index f4b7491..3856e0a 100644 --- a/concat_series_test.go +++ b/concat_series_test.go @@ -3,39 +3,40 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) 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: seq.Range(1.0, 10.0), + YValues: seq.Range(1.0, 10.0), } s2 := ContinuousSeries{ - XValues: LinearRange(11, 20.0), - YValues: LinearRange(10.0, 1.0), + XValues: seq.Range(11, 20.0), + YValues: seq.Range(10.0, 1.0), } s3 := ContinuousSeries{ - XValues: LinearRange(21, 30.0), - YValues: LinearRange(1.0, 10.0), + XValues: seq.Range(21, 30.0), + YValues: seq.Range(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) } diff --git a/continuous_range.go b/continuous_range.go index 517b727..99fa939 100644 --- a/continuous_range.go +++ b/continuous_range.go @@ -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) } diff --git a/continuous_range_test.go b/continuous_range_test.go index aaa7ec4..fe3b233 100644 --- a/continuous_range_test.go +++ b/continuous_range_test.go @@ -3,20 +3,21 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" ) 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 = util.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)) } diff --git a/continuous_series.go b/continuous_series.go index 73c7ab7..bca80de 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -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 @@ -43,11 +36,6 @@ func (cs ContinuousSeries) GetValues(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) { 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 } diff --git a/continuous_series_test.go b/continuous_series_test.go index ec6193b..89af981 100644 --- a/continuous_series_test.go +++ b/continuous_series_test.go @@ -4,35 +4,36 @@ import ( "fmt" "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) 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: seq.Range(1.0, 10.0), + YValues: seq.Range(1.0, 10.0), } - testutil.AssertEqual(t, "Test Series", cs.GetName()) - testutil.AssertEqual(t, 10, cs.Len()) + assert.Equal("Test Series", cs.GetName()) + assert.Equal(10, cs.Len()) x0, y0 := cs.GetValues(0) - testutil.AssertEqual(t, 1.0, x0) - testutil.AssertEqual(t, 1.0, y0) + 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) + 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) + 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 +45,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: seq.Range(1.0, 10.0), + YValues: seq.Range(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: seq.Range(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: seq.Range(1.0, 10.0), } - testutil.AssertNotNil(t, cs.Validate()) + assert.NotNil(cs.Validate()) } diff --git a/debug.test b/debug.test new file mode 100755 index 0000000..8ac0d3f Binary files /dev/null and b/debug.test differ diff --git a/donut_chart.go b/donut_chart.go deleted file mode 100644 index f5a7854..0000000 --- a/donut_chart.go +++ /dev/null @@ -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, - } -} diff --git a/donut_chart_test.go b/donut_chart_test.go deleted file mode 100644 index 0d1946b..0000000 --- a/donut_chart_test.go +++ /dev/null @@ -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) -} diff --git a/draw.go b/draw.go index e188079..70b9576 100644 --- a/draw.go +++ b/draw.go @@ -2,6 +2,8 @@ package chart import ( "math" + + util "github.com/wcharczuk/go-chart/util" ) var ( @@ -38,8 +40,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style 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, util.Math.MinInt(cb, cb-yv0)) + r.LineTo(x0, util.Math.MinInt(cb, cb-yv0)) r.LineTo(x0, y0) r.Fill() } @@ -166,14 +168,73 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s } } +func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, cs CandlestickSeries) { + if cs.Len() == 0 { + return + } + + candleValues := cs.GetCandleValues() + + cb := canvasBox.Bottom + cl := canvasBox.Left + + var cv CandleValue + for index := 0; index < len(candleValues); index++ { + cv = candleValues[index] + + y0 := yrange.Translate(cv.Open) + y1 := yrange.Translate(cv.Close) + + x0 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEOpen(), cv.Timestamp))) + x1 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEClose(), cv.Timestamp))) + + x := x0 + ((x1 - x0) >> 1) + + // draw open / close box. + if cv.Open < cv.Close { + d.Box(r, Box{ + Top: cb - y0, + Left: x0, + Right: x1, + Bottom: cb - y1, + }, style.InheritFrom(Style{FillColor: ColorAlternateGreen})) + } else { + d.Box(r, Box{ + Top: cb - y1, + Left: x0, + Right: x1, + Bottom: cb - y0, + }, style.InheritFrom(Style{FillColor: ColorRed})) + } + + // draw high / low t bars + y0 = yrange.Translate(cv.High) + y1 = yrange.Translate(cv.Low) + + style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r) + + r.MoveTo(x0, cb-y0) + r.LineTo(x1, cb-y0) + r.Stroke() + + r.MoveTo(x, cb-y0) + r.LineTo(x, cb-y1) + r.Stroke() + + r.MoveTo(x0, cb-y1) + r.LineTo(x1, cb-y1) + r.Stroke() + } +} + // MeasureAnnotation measures how big an annotation would be. func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box { style.WriteToRenderer(r) defer r.ResetStyle() textBox := r.MeasureText(label) - textWidth := textBox.Width() - textHeight := textBox.Height() + textWidth := int(textBox.Width()) + textHeight := int(textBox.Height()) halfTextHeight := textHeight >> 1 pt := style.Padding.GetTop(DefaultAnnotationPadding.Top) @@ -201,8 +262,8 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab defer r.ResetStyle() textBox := r.MeasureText(label) - textWidth := textBox.Width() - halfTextHeight := textBox.Height() >> 1 + textWidth := int(textBox.Width()) + halfTextHeight := int(textBox.Height()) >> 1 style.GetFillAndStrokeOptions().WriteToRenderer(r) @@ -253,17 +314,17 @@ func (d draw) Box(r Renderer, b Box, s Style) { } func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) { - d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s) + d.Box2d(r, b.Corners().Rotate(thetaDegrees), s) } -func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) { +func (d draw) Box2d(r Renderer, bc Box2d, s Style) { s.GetFillAndStrokeOptions().WriteToRenderer(r) defer r.ResetStyle() - r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y) - r.LineTo(bc.TopRight.X, bc.TopRight.Y) - r.LineTo(bc.BottomRight.X, bc.BottomRight.Y) - r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y) + r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y)) + r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y)) + r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y)) + r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y)) r.Close() r.FillStroke() } @@ -276,7 +337,7 @@ func (d draw) Text(r Renderer, text string, x, y int, style Style) { r.Text(text, x, y) } -func (d draw) MeasureText(r Renderer, text string, style Style) Box { +func (d draw) MeasureText(r Renderer, text string, style Style) Box2d { style.GetTextOptions().WriteToRenderer(r) defer r.ResetStyle() @@ -295,11 +356,9 @@ 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() + y = y - int(linesBox.Height()) + case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline: + y = (y - int(linesBox.Height())) >> 1 } var tx, ty int @@ -307,19 +366,19 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) { lineBox := r.MeasureText(line) switch style.GetTextHorizontalAlign() { case TextHorizontalAlignCenter: - tx = box.Left + ((box.Width() - lineBox.Width()) >> 1) + tx = box.Left + ((int(box.Width()) - int(lineBox.Width())) >> 1) case TextHorizontalAlignRight: - tx = box.Right - lineBox.Width() + tx = box.Right - int(lineBox.Width()) default: tx = box.Left } if style.TextRotationDegrees == 0 { - ty = y + lineBox.Height() + ty = y + int(lineBox.Height()) } else { ty = y } r.Text(line, tx, ty) - y += lineBox.Height() + style.GetTextLineSpacing() + y += int(lineBox.Height()) + style.GetTextLineSpacing() } } diff --git a/drawing/color.go b/drawing/color.go index 4b865d7..cfe0924 100644 --- a/drawing/color.go +++ b/drawing/color.go @@ -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.+),(?P.+),(?P.+),(?P.+)\)`) - -// 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.+),(?P.+),(?P.+)\)`) - -// 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 diff --git a/drawing/color_test.go b/drawing/color_test.go index bb01ee1..bdedd02 100644 --- a/drawing/color_test.go +++ b/drawing/color_test.go @@ -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()) } diff --git a/drawing/curve_test.go b/drawing/curve_test.go index f79cb79..5c22cc1 100644 --- a/drawing/curve_test.go +++ b/drawing/curve_test.go @@ -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()) } diff --git a/ema_series.go b/ema_series.go index 44415b5..ceaec39 100644 --- a/ema_series.go +++ b/ema_series.go @@ -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 @@ -73,19 +66,6 @@ func (ema *EMASeries) GetValues(index int) (x, y float64) { 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, // and recomputing the last moving average chunk. func (ema *EMASeries) GetLastValues() (x, y float64) { diff --git a/ema_series_test.go b/ema_series_test.go index e1e5831..e2867d0 100644 --- a/ema_series_test.go +++ b/ema_series_test.go @@ -3,11 +3,12 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) var ( - emaXValues = LinearRange(1.0, 50.0) + emaXValues = seq.Range(1.0, 50.0) emaYValues = []float64{ 1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 4, 3, 2, @@ -73,13 +74,13 @@ var ( ) func TestEMASeries(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mockSeries := mockValuesProvider{ emaXValues, emaYValues, } - testutil.AssertEqual(t, 50, mockSeries.Len()) + assert.Equal(50, mockSeries.Len()) ema := &EMASeries{ InnerSeries: mockSeries, @@ -87,7 +88,7 @@ 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++ { @@ -96,10 +97,10 @@ func TestEMASeries(t *testing.T) { } 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) + assert.Equal(50.0, lvx) + assert.InDelta(lvy, emaExpected[49], emaDelta) } diff --git a/examples/annotations/output.png b/examples/annotations/output.png deleted file mode 100644 index 3127025..0000000 Binary files a/examples/annotations/output.png and /dev/null differ diff --git a/examples/axes/output.png b/examples/axes/output.png deleted file mode 100644 index 9a16db0..0000000 Binary files a/examples/axes/output.png and /dev/null differ diff --git a/examples/axes_labels/output.png b/examples/axes_labels/output.png deleted file mode 100644 index 5ca998a..0000000 Binary files a/examples/axes_labels/output.png and /dev/null differ diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go deleted file mode 100644 index 36b45a6..0000000 --- a/examples/bar_chart/main.go +++ /dev/null @@ -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) -} diff --git a/examples/bar_chart/output.png b/examples/bar_chart/output.png deleted file mode 100644 index 840c46e..0000000 Binary files a/examples/bar_chart/output.png and /dev/null differ diff --git a/examples/bar_chart_base_value/main.go b/examples/bar_chart_base_value/main.go deleted file mode 100644 index 52fa940..0000000 --- a/examples/bar_chart_base_value/main.go +++ /dev/null @@ -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) -} diff --git a/examples/bar_chart_base_value/output.png b/examples/bar_chart_base_value/output.png deleted file mode 100644 index 94231c8..0000000 Binary files a/examples/bar_chart_base_value/output.png and /dev/null differ diff --git a/examples/basic/main.go b/examples/basic/main.go deleted file mode 100644 index e05df6a..0000000 --- a/examples/basic/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func main() { - 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}, - }, - }, - } - f, _ := os.Create("output.png") - defer f.Close() - graph.Render(chart.PNG, f) -} diff --git a/examples/basic/output.png b/examples/basic/output.png deleted file mode 100644 index fa0573a..0000000 Binary files a/examples/basic/output.png and /dev/null differ diff --git a/examples/benchmark_line_charts/main.go b/examples/benchmark_line_charts/main.go deleted file mode 100644 index e79b4d4..0000000 --- a/examples/benchmark_line_charts/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "fmt" - "math/rand" - "os" - "time" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func random(min, max float64) float64 { - return rand.Float64()*(max-min) + min -} - -func main() { - numValues := 1024 - numSeries := 100 - series := make([]chart.Series, numSeries) - - for i := 0; i < numSeries; i++ { - xValues := make([]time.Time, numValues) - yValues := make([]float64, numValues) - - for j := 0; j < numValues; j++ { - xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1) - yValues[j] = random(float64(-500), float64(500)) - } - - series[i] = chart.TimeSeries{ - Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i), - XValues: xValues, - YValues: yValues, - } - } - - graph := chart.Chart{ - XAxis: chart.XAxis{ - Name: "Time", - }, - YAxis: chart.YAxis{ - Name: "Value", - }, - Series: series, - } - - f, _ := os.Create("output.png") - defer f.Close() - graph.Render(chart.PNG, f) -} diff --git a/examples/benchmark_line_charts/output.png b/examples/benchmark_line_charts/output.png deleted file mode 100644 index 588d6a2..0000000 Binary files a/examples/benchmark_line_charts/output.png and /dev/null differ diff --git a/examples/css_classes/main.go b/examples/css_classes/main.go deleted file mode 100644 index f34a3da..0000000 --- a/examples/css_classes/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "git.smarteching.com/zeni/go-chart/v2" -) - -// Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example - -func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) { - res.Write([]byte( - "" + - "" + - "" + - "")) - - pie := chart.PieChart{ - // Notes: * Setting ClassName will cause all other inline styles to be dropped! - // * The following type classes may be added additionally: stroke, fill, text - Background: chart.Style{ClassName: "background"}, - Canvas: chart.Style{ - ClassName: "canvas", - }, - Width: 512, - Height: 512, - Values: []chart.Value{ - {Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}}, - {Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}}, - {Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}}, - }, - } - - err := pie.Render(chart.SVG, res) - if err != nil { - fmt.Printf("Error rendering pie chart: %v\n", err) - } - res.Write([]byte("")) -} - -func css(res http.ResponseWriter, req *http.Request) { - res.Header().Set("Content-Type", "text/css") - res.Write([]byte("svg .background { fill: white; }" + - "svg .canvas { fill: white; }" + - "svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" + - "svg .green.fill.stroke { fill: green; stroke: lightgreen; }" + - "svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" + - "svg .blue.text { fill: white; }" + - "svg .green.text { fill: white; }" + - "svg .gray.text { fill: white; }")) -} - -func main() { - http.HandleFunc("/", inlineSVGWithClasses) - http.HandleFunc("/main.css", css) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/examples/custom_formatters/output.png b/examples/custom_formatters/output.png deleted file mode 100644 index 8d8b4e9..0000000 Binary files a/examples/custom_formatters/output.png and /dev/null differ diff --git a/examples/custom_padding/main.go b/examples/custom_padding/main.go deleted file mode 100644 index e0d6452..0000000 --- a/examples/custom_padding/main.go +++ /dev/null @@ -1,34 +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() { - graph := chart.Chart{ - Background: chart.Style{ - Padding: chart.Box{ - Top: 50, - Left: 25, - Right: 25, - Bottom: 10, - }, - FillColor: drawing.ColorFromHex("efefef"), - }, - Series: []chart.Series{ - chart.ContinuousSeries{ - XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), - YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(100).WithMax(512)}.Values(), - }, - }, - } - - f, _ := os.Create("output.png") - defer f.Close() - graph.Render(chart.PNG, f) -} diff --git a/examples/custom_padding/output.png b/examples/custom_padding/output.png deleted file mode 100644 index 63c17cc..0000000 Binary files a/examples/custom_padding/output.png and /dev/null differ diff --git a/examples/custom_ranges/output.png b/examples/custom_ranges/output.png deleted file mode 100644 index 8ea7da4..0000000 Binary files a/examples/custom_ranges/output.png and /dev/null differ diff --git a/examples/custom_styles/output.png b/examples/custom_styles/output.png deleted file mode 100644 index 981745d..0000000 Binary files a/examples/custom_styles/output.png and /dev/null differ diff --git a/examples/custom_stylesheets/inlineOutput.svg b/examples/custom_stylesheets/inlineOutput.svg deleted file mode 100644 index fdb2515..0000000 --- a/examples/custom_stylesheets/inlineOutput.svg +++ /dev/null @@ -1,21 +0,0 @@ -\nBlueGreenGray \ No newline at end of file diff --git a/examples/custom_stylesheets/main.go b/examples/custom_stylesheets/main.go deleted file mode 100644 index 51b8bbf..0000000 --- a/examples/custom_stylesheets/main.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "git.smarteching.com/zeni/go-chart/v2" -) - -const style = "svg .background { fill: white; }" + - "svg .canvas { fill: white; }" + - "svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" + - "svg .green.fill.stroke { fill: green; stroke: lightgreen; }" + - "svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" + - "svg .blue.text { fill: white; }" + - "svg .green.text { fill: white; }" + - "svg .gray.text { fill: white; }" - -func svgWithCustomInlineCSS(res http.ResponseWriter, _ *http.Request) { - res.Header().Set("Content-Type", chart.ContentTypeSVG) - - // Render the CSS with custom css - err := pieChart().Render(chart.SVGWithCSS(style, ""), res) - if err != nil { - fmt.Printf("Error rendering pie chart: %v\n", err) - } -} - -func svgWithCustomInlineCSSNonce(res http.ResponseWriter, _ *http.Request) { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src - // This should be randomly generated on every request! - const nonce = "RAND0MBASE64" - - res.Header().Set("Content-Security-Policy", fmt.Sprintf("style-src 'nonce-%s'", nonce)) - res.Header().Set("Content-Type", chart.ContentTypeSVG) - - // Render the CSS with custom css and a nonce. - // Try changing the nonce to a different string - your browser should block the CSS. - err := pieChart().Render(chart.SVGWithCSS(style, nonce), res) - if err != nil { - fmt.Printf("Error rendering pie chart: %v\n", err) - } -} - -func svgWithCustomExternalCSS(res http.ResponseWriter, _ *http.Request) { - // Add external CSS - res.Write([]byte( - `` + - `` + - ``)) - - res.Header().Set("Content-Type", chart.ContentTypeSVG) - err := pieChart().Render(chart.SVG, res) - if err != nil { - fmt.Printf("Error rendering pie chart: %v\n", err) - } -} - -func pieChart() chart.PieChart { - return chart.PieChart{ - // Note that setting ClassName will cause all other inline styles to be dropped! - Background: chart.Style{ClassName: "background"}, - Canvas: chart.Style{ - ClassName: "canvas", - }, - Width: 512, - Height: 512, - Values: []chart.Value{ - {Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}}, - {Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}}, - {Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}}, - }, - } -} - -func css(res http.ResponseWriter, req *http.Request) { - res.Header().Set("Content-Type", "text/css") - res.Write([]byte(style)) -} - -func main() { - http.HandleFunc("/", svgWithCustomInlineCSS) - http.HandleFunc("/nonce", svgWithCustomInlineCSSNonce) - http.HandleFunc("/external", svgWithCustomExternalCSS) - http.HandleFunc("/main.css", css) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/examples/custom_ticks/output.png b/examples/custom_ticks/output.png deleted file mode 100644 index fc9d7b6..0000000 Binary files a/examples/custom_ticks/output.png and /dev/null differ diff --git a/examples/descending/output.png b/examples/descending/output.png deleted file mode 100644 index 2f30474..0000000 Binary files a/examples/descending/output.png and /dev/null differ diff --git a/examples/donut_chart/main.go b/examples/donut_chart/main.go deleted file mode 100644 index 3a4e804..0000000 --- a/examples/donut_chart/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func main() { - pie := chart.DonutChart{ - Width: 512, - Height: 512, - Values: []chart.Value{ - {Value: 5, Label: "Blue"}, - {Value: 5, Label: "Green"}, - {Value: 4, Label: "Gray"}, - {Value: 4, Label: "Orange"}, - {Value: 3, Label: "Deep Blue"}, - {Value: 3, Label: "test"}, - }, - } - - f, _ := os.Create("output.png") - defer f.Close() - pie.Render(chart.PNG, f) -} diff --git a/examples/donut_chart/output.png b/examples/donut_chart/output.png deleted file mode 100644 index e682501..0000000 Binary files a/examples/donut_chart/output.png and /dev/null differ diff --git a/examples/donut_chart/reg.svg b/examples/donut_chart/reg.svg deleted file mode 100644 index f14c2af..0000000 --- a/examples/donut_chart/reg.svg +++ /dev/null @@ -1,25 +0,0 @@ -\nBlueTwoOne \ No newline at end of file diff --git a/examples/horizontal_stacked_bar/main.go b/examples/horizontal_stacked_bar/main.go deleted file mode 100644 index d695b46..0000000 --- a/examples/horizontal_stacked_bar/main.go +++ /dev/null @@ -1,222 +0,0 @@ -package main - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func main() { - chart.DefaultBackgroundColor = chart.ColorTransparent - chart.DefaultCanvasColor = chart.ColorTransparent - - barWidth := 80 - - var ( - colorWhite = drawing.Color{R: 241, G: 241, B: 241, A: 255} - colorMariner = drawing.Color{R: 60, G: 100, B: 148, A: 255} - colorLightSteelBlue = drawing.Color{R: 182, G: 195, B: 220, A: 255} - colorPoloBlue = drawing.Color{R: 126, G: 155, B: 200, A: 255} - colorSteelBlue = drawing.Color{R: 73, G: 120, B: 177, A: 255} - ) - - stackedBarChart := chart.StackedBarChart{ - Title: "Quarterly Sales", - TitleStyle: chart.Shown(), - Background: chart.Style{ - Padding: chart.Box{ - Top: 75, - }, - }, - Width: 800, - Height: 600, - XAxis: chart.Shown(), - YAxis: chart.Shown(), - BarSpacing: 40, - IsHorizontal: true, - Bars: []chart.StackedBar{ - { - Name: "Q1", - Width: barWidth, - Values: []chart.Value{ - { - Label: "32K", - Value: 32, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "46K", - Value: 46, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "48K", - Value: 48, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "42K", - Value: 42, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q2", - Width: barWidth, - Values: []chart.Value{ - { - Label: "45K", - Value: 45, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "60K", - Value: 60, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "62K", - Value: 62, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "53K", - Value: 53, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q3", - Width: barWidth, - Values: []chart.Value{ - { - Label: "54K", - Value: 54, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "58K", - Value: 58, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "55K", - Value: 55, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "47K", - Value: 47, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q4", - Width: barWidth, - Values: []chart.Value{ - { - Label: "46K", - Value: 46, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "70K", - Value: 70, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "74K", - Value: 74, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "60K", - Value: 60, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - }, - } - - pngFile, err := os.Create("output.png") - if err != nil { - panic(err) - } - - if err := stackedBarChart.Render(chart.PNG, pngFile); err != nil { - panic(err) - } - - if err := pngFile.Close(); err != nil { - panic(err) - } -} diff --git a/examples/horizontal_stacked_bar/output.png b/examples/horizontal_stacked_bar/output.png deleted file mode 100644 index d8ad123..0000000 Binary files a/examples/horizontal_stacked_bar/output.png and /dev/null differ diff --git a/examples/legend/output.png b/examples/legend/output.png deleted file mode 100644 index 82ebfa8..0000000 Binary files a/examples/legend/output.png and /dev/null differ diff --git a/examples/legend_left/output.png b/examples/legend_left/output.png deleted file mode 100644 index 94ff2b5..0000000 Binary files a/examples/legend_left/output.png and /dev/null differ diff --git a/examples/linear_regression/output.png b/examples/linear_regression/output.png deleted file mode 100644 index a0ff7b6..0000000 Binary files a/examples/linear_regression/output.png and /dev/null differ diff --git a/examples/logarithmic_axes/main.go b/examples/logarithmic_axes/main.go deleted file mode 100644 index 2a19609..0000000 --- a/examples/logarithmic_axes/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func main() { - - /* - In this example we set the primary YAxis to have logarithmic range. - */ - - graph := chart.Chart{ - Background: chart.Style{ - Padding: chart.Box{ - Top: 20, - Left: 20, - }, - }, - Series: []chart.Series{ - chart.ContinuousSeries{ - Name: "A test series", - XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - YValues: []float64{1, 10, 100, 1000, 10000}, - }, - }, - YAxis: chart.YAxis{ - Style: chart.Shown(), - NameStyle: chart.Shown(), - Range: &chart.LogarithmicRange{}, - }, - } - - f, _ := os.Create("output.png") - defer f.Close() - graph.Render(chart.PNG, f) -} diff --git a/examples/logarithmic_axes/output.png b/examples/logarithmic_axes/output.png deleted file mode 100644 index 4462b8d..0000000 Binary files a/examples/logarithmic_axes/output.png and /dev/null differ diff --git a/examples/min_max/output.png b/examples/min_max/output.png deleted file mode 100644 index 738b2fd..0000000 Binary files a/examples/min_max/output.png and /dev/null differ diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go deleted file mode 100644 index 22539da..0000000 --- a/examples/pie_chart/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func main() { - pie := chart.PieChart{ - Width: 512, - Height: 512, - Values: []chart.Value{ - {Value: 5, Label: "Blue"}, - {Value: 5, Label: "Green"}, - {Value: 4, Label: "Gray"}, - {Value: 4, Label: "Orange"}, - {Value: 3, Label: "Deep Blue"}, - {Value: 3, Label: "??"}, - {Value: 1, Label: "!!"}, - }, - } - - f, _ := os.Create("output.png") - defer f.Close() - pie.Render(chart.PNG, f) -} diff --git a/examples/pie_chart/reg.svg b/examples/pie_chart/reg.svg deleted file mode 100644 index 6b8d2ff..0000000 --- a/examples/pie_chart/reg.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/examples/poly_regression/output.png b/examples/poly_regression/output.png deleted file mode 100644 index 84c56f9..0000000 Binary files a/examples/poly_regression/output.png and /dev/null differ diff --git a/examples/request_timings/main.go b/examples/request_timings/main.go deleted file mode 100644 index 0290b65..0000000 --- a/examples/request_timings/main.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -//go:generate go run main.go - -import ( - "fmt" - "net/http" - "os" - "strconv" - "time" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func main() { - log := chart.NewLogger() - drawChart(log) -} - -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.ReadLines("requests.csv", func(line string) error { - parts := chart.SplitCSV(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) - return nil - }) - if err != nil { - fmt.Println(err.Error()) - } - return xvalues, yvalues -} - -func releases() []chart.GridLine { - return []chart.GridLine{ - {Value: chart.TimeToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))}, - {Value: chart.TimeToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))}, - {Value: chart.TimeToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))}, - {Value: chart.TimeToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))}, - {Value: chart.TimeToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))}, - {Value: chart.TimeToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))}, - } -} - -func drawChart(log chart.Logger) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - xvalues, yvalues := readData() - mainSeries := chart.TimeSeries{ - Name: "Prod Request Timings", - Style: chart.Style{ - StrokeColor: chart.ColorBlue, - FillColor: chart.ColorBlue.WithAlpha(100), - }, - XValues: xvalues, - YValues: yvalues, - } - - linreg := &chart.LinearRegressionSeries{ - Name: "Linear Regression", - Style: chart.Style{ - StrokeColor: chart.ColorAlternateBlue, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: mainSeries, - } - - sma := &chart.SMASeries{ - Name: "SMA", - Style: chart.Style{ - StrokeColor: chart.ColorRed, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: mainSeries, - } - - graph := chart.Chart{ - Log: log, - Width: 1280, - Height: 720, - Background: chart.Style{ - Padding: chart.Box{ - Top: 50, - }, - }, - YAxis: chart.YAxis{ - Name: "Elapsed Millis", - TickStyle: chart.Style{ - TextRotationDegrees: 45.0, - }, - ValueFormatter: func(v interface{}) string { - return fmt.Sprintf("%d ms", int(v.(float64))) - }, - }, - XAxis: chart.XAxis{ - ValueFormatter: chart.TimeHourValueFormatter, - GridMajorStyle: chart.Style{ - StrokeColor: chart.ColorAlternateGray, - StrokeWidth: 1.0, - }, - GridLines: releases(), - }, - Series: []chart.Series{ - mainSeries, - linreg, - chart.LastValueAnnotationSeries(linreg), - sma, - chart.LastValueAnnotationSeries(sma), - }, - } - - graph.Elements = []chart.Renderable{chart.LegendThin(&graph)} - - f, _ := os.Create("output.png") - defer f.Close() - graph.Render(chart.PNG, f) - } -} diff --git a/examples/rerender/main.go b/examples/rerender/main.go deleted file mode 100644 index 50a960e..0000000 --- a/examples/rerender/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "log" - "net/http" - "sync" - "time" - - "git.smarteching.com/zeni/go-chart/v2" -) - -var lock sync.Mutex -var graph *chart.Chart -var ts *chart.TimeSeries - -func addData(t time.Time, e time.Duration) { - lock.Lock() - ts.XValues = append(ts.XValues, t) - ts.YValues = append(ts.YValues, chart.TimeMillis(e)) - lock.Unlock() -} - -func drawChart(res http.ResponseWriter, req *http.Request) { - start := time.Now() - defer func() { - addData(start, time.Since(start)) - }() - if len(ts.XValues) == 0 { - http.Error(res, "no data (yet)", http.StatusBadRequest) - return - } - res.Header().Set("Content-Type", "image/png") - if err := graph.Render(chart.PNG, res); err != nil { - log.Printf("%v", err) - } -} - -func main() { - ts = &chart.TimeSeries{ - XValues: []time.Time{}, - YValues: []float64{}, - } - graph = &chart.Chart{ - Series: []chart.Series{ts}, - } - http.HandleFunc("/", drawChart) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/examples/simple_moving_average/output.png b/examples/simple_moving_average/output.png deleted file mode 100644 index 1c00f38..0000000 Binary files a/examples/simple_moving_average/output.png and /dev/null differ diff --git a/examples/stacked_bar_labels/main.go b/examples/stacked_bar_labels/main.go deleted file mode 100644 index 2af9e21..0000000 --- a/examples/stacked_bar_labels/main.go +++ /dev/null @@ -1,221 +0,0 @@ -package main - -import ( - "os" - - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func main() { - chart.DefaultBackgroundColor = chart.ColorTransparent - chart.DefaultCanvasColor = chart.ColorTransparent - - barWidth := 120 - - var ( - colorWhite = drawing.Color{R: 241, G: 241, B: 241, A: 255} - colorMariner = drawing.Color{R: 60, G: 100, B: 148, A: 255} - colorLightSteelBlue = drawing.Color{R: 182, G: 195, B: 220, A: 255} - colorPoloBlue = drawing.Color{R: 126, G: 155, B: 200, A: 255} - colorSteelBlue = drawing.Color{R: 73, G: 120, B: 177, A: 255} - ) - - stackedBarChart := chart.StackedBarChart{ - Title: "Quarterly Sales", - TitleStyle: chart.Shown(), - Background: chart.Style{ - Padding: chart.Box{ - Top: 100, - }, - }, - Width: 810, - Height: 500, - XAxis: chart.Shown(), - YAxis: chart.Shown(), - BarSpacing: 50, - Bars: []chart.StackedBar{ - { - Name: "Q1", - Width: barWidth, - Values: []chart.Value{ - { - Label: "32K", - Value: 32, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "46K", - Value: 46, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "48K", - Value: 48, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "42K", - Value: 42, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q2", - Width: barWidth, - Values: []chart.Value{ - { - Label: "45K", - Value: 45, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "60K", - Value: 60, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "62K", - Value: 62, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "53K", - Value: 53, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q3", - Width: barWidth, - Values: []chart.Value{ - { - Label: "54K", - Value: 54, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "58K", - Value: 58, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "55K", - Value: 55, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "47K", - Value: 47, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - { - Name: "Q4", - Width: barWidth, - Values: []chart.Value{ - { - Label: "46K", - Value: 46, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorMariner, - FontColor: colorWhite, - }, - }, - { - Label: "70K", - Value: 70, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorLightSteelBlue, - FontColor: colorWhite, - }, - }, - { - Label: "74K", - Value: 74, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorPoloBlue, - FontColor: colorWhite, - }, - }, - { - Label: "60K", - Value: 60, - Style: chart.Style{ - StrokeWidth: .01, - FillColor: colorSteelBlue, - FontColor: colorWhite, - }, - }, - }, - }, - }, - } - - pngFile, err := os.Create("output.png") - if err != nil { - panic(err) - } - - if err := stackedBarChart.Render(chart.PNG, pngFile); err != nil { - panic(err) - } - - if err := pngFile.Close(); err != nil { - panic(err) - } -} diff --git a/examples/stacked_bar_labels/output.png b/examples/stacked_bar_labels/output.png deleted file mode 100644 index d5f7e97..0000000 Binary files a/examples/stacked_bar_labels/output.png and /dev/null differ diff --git a/examples/stock_analysis/output.png b/examples/stock_analysis/output.png deleted file mode 100644 index dc804c8..0000000 Binary files a/examples/stock_analysis/output.png and /dev/null differ diff --git a/examples/twopoint/output.png b/examples/twopoint/output.png deleted file mode 100644 index 10059e7..0000000 Binary files a/examples/twopoint/output.png and /dev/null differ diff --git a/fileutil.go b/fileutil.go deleted file mode 100644 index f1a7768..0000000 --- a/fileutil.go +++ /dev/null @@ -1,49 +0,0 @@ -package chart - -import ( - "bufio" - "io" - "os" -) - -// ReadLines reads a file and calls the handler for each line. -func ReadLines(filePath string, handler func(string) error) error { - f, err := os.Open(filePath) - if err != nil { - return err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - err = handler(line) - if err != nil { - return err - } - } - return nil -} - -// ReadChunks reads a file in `chunkSize` pieces, dispatched to the handler. -func ReadChunks(filePath string, chunkSize int, handler func([]byte) error) error { - f, err := os.Open(filePath) - if err != nil { - return err - } - defer f.Close() - - chunk := make([]byte, chunkSize) - for { - readBytes, err := f.Read(chunk) - if err == io.EOF { - break - } - readData := chunk[:readBytes] - err = handler(readData) - if err != nil { - return err - } - } - return nil -} diff --git a/first_value_annotation.go b/first_value_annotation.go deleted file mode 100644 index 2b214b3..0000000 --- a/first_value_annotation.go +++ /dev/null @@ -1,37 +0,0 @@ -package chart - -import "fmt" - -// FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation. -func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { - var vf ValueFormatter - if len(vfs) > 0 { - vf = vfs[0] - } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { - _, vf = typed.GetValueFormatters() - } else { - vf = FloatValueFormatter - } - - var firstValue Value2 - if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped { - firstValue.XValue, firstValue.YValue = typed.GetFirstValues() - firstValue.Label = vf(firstValue.YValue) - } else { - firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0) - firstValue.Label = vf(firstValue.YValue) - } - - var seriesName string - var seriesStyle Style - if typed, isTyped := innerSeries.(Series); isTyped { - seriesName = fmt.Sprintf("%s - First Value", typed.GetName()) - seriesStyle = typed.GetStyle() - } - - return AnnotationSeries{ - Name: seriesName, - Style: seriesStyle, - Annotations: []Value2{firstValue}, - } -} diff --git a/first_value_annotation_test.go b/first_value_annotation_test.go deleted file mode 100644 index ea1d26d..0000000 --- a/first_value_annotation_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestFirstValueAnnotation(t *testing.T) { - // replaced new assertions helper - - series := ContinuousSeries{ - XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - YValues: []float64{5.0, 3.0, 3.0, 2.0, 1.0}, - } - - fva := FirstValueAnnotation(series) - testutil.AssertNotEmpty(t, fva.Annotations) - fvaa := fva.Annotations[0] - testutil.AssertEqual(t, 1, fvaa.XValue) - testutil.AssertEqual(t, 5, fvaa.YValue) -} diff --git a/font.go b/font.go index 8cb42c4..a82880c 100644 --- a/font.go +++ b/font.go @@ -3,8 +3,8 @@ package chart import ( "sync" - "git.smarteching.com/zeni/go-chart/v2/roboto" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/roboto" ) var ( diff --git a/go.mod b/go.mod deleted file mode 100644 index 53e8c70..0000000 --- a/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.smarteching.com/zeni/go-chart/v2 - -go 1.23.1 - -require ( - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - golang.org/x/image v0.21.0 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 928ee08..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= diff --git a/grid_line_test.go b/grid_line_test.go index cc508e1..ac57f7b 100644 --- a/grid_line_test.go +++ b/grid_line_test.go @@ -3,11 +3,11 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) func TestGenerateGridLines(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) ticks := []Tick{ {Value: 1.0, Label: "1.0"}, @@ -17,8 +17,8 @@ func TestGenerateGridLines(t *testing.T) { } gl := GenerateGridLines(ticks, Style{}, Style{}) - testutil.AssertLen(t, gl, 2) + assert.Len(gl, 2) - testutil.AssertEqual(t, 2.0, gl[0].Value) - testutil.AssertEqual(t, 3.0, gl[1].Value) + assert.Equal(2.0, gl[0].Value) + assert.Equal(3.0, gl[1].Value) } diff --git a/histogram_series_test.go b/histogram_series_test.go index 75e7db6..744ebe1 100644 --- a/histogram_series_test.go +++ b/histogram_series_test.go @@ -3,16 +3,17 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) func TestHistogramSeries(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) cs := ContinuousSeries{ Name: "Test Series", - XValues: LinearRange(1.0, 20.0), - YValues: LinearRange(10.0, -10.0), + XValues: seq.Range(1.0, 20.0), + YValues: seq.Range(10.0, -10.0), } hs := HistogramSeries{ @@ -22,10 +23,10 @@ func TestHistogramSeries(t *testing.T) { for x := 0; x < hs.Len(); x++ { csx, csy := cs.GetValues(0) hsx, hsy1, hsy2 := hs.GetBoundedValues(0) - testutil.AssertEqual(t, csx, hsx) - testutil.AssertTrue(t, hsy1 > 0) - testutil.AssertTrue(t, hsy2 <= 0) - testutil.AssertTrue(t, csy < 0 || (csy > 0 && csy == hsy1)) - testutil.AssertTrue(t, csy > 0 || (csy < 0 && csy == hsy2)) + assert.Equal(csx, hsx) + assert.True(hsy1 > 0) + assert.True(hsy2 <= 0) + assert.True(csy < 0 || (csy > 0 && csy == hsy1)) + assert.True(csy > 0 || (csy < 0 && csy == hsy2)) } } diff --git a/jet.go b/jet.go index a539a29..a948525 100644 --- a/jet.go +++ b/jet.go @@ -1,6 +1,6 @@ package chart -import "git.smarteching.com/zeni/go-chart/v2/drawing" +import "github.com/wcharczuk/go-chart/drawing" // Jet is a color map provider based on matlab's jet color map. func Jet(v, vmin, vmax float64) drawing.Color { diff --git a/last_value_annotation_series.go b/last_value_annotation_series.go index 550c367..f3d4b46 100644 --- a/last_value_annotation_series.go +++ b/last_value_annotation_series.go @@ -2,8 +2,8 @@ package chart import "fmt" -// LastValueAnnotationSeries returns an annotation series of just the last value of a value provider. -func LastValueAnnotationSeries(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { +// LastValueAnnotation returns an annotation series of just the last value of a value provider. +func LastValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { var vf ValueFormatter if len(vfs) > 0 { vf = vfs[0] diff --git a/last_value_annotation_series_test.go b/last_value_annotation_series_test.go deleted file mode 100644 index afc2409..0000000 --- a/last_value_annotation_series_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestLastValueAnnotationSeries(t *testing.T) { - // replaced new assertions helper - - series := ContinuousSeries{ - XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - YValues: []float64{5.0, 3.0, 3.0, 2.0, 1.0}, - } - - lva := LastValueAnnotationSeries(series) - testutil.AssertNotEmpty(t, lva.Annotations) - lvaa := lva.Annotations[0] - testutil.AssertEqual(t, 5, lvaa.XValue) - testutil.AssertEqual(t, 1, lvaa.YValue) -} diff --git a/legend.go b/legend.go index a1dec06..5cac0a1 100644 --- a/legend.go +++ b/legend.go @@ -1,7 +1,8 @@ package chart import ( - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/drawing" + "github.com/wcharczuk/go-chart/util" ) // Legend returns a legend renderable function. @@ -35,7 +36,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if !s.GetStyle().Hidden { + if s.GetStyle().IsZero() || s.GetStyle().Show { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) @@ -66,9 +67,9 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { if labelCount > 0 { legendContent.Bottom += DefaultMinimumTickVerticalSpacing } - legendContent.Bottom += tb.Height() - right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum - legendContent.Right = MaxInt(legendContent.Right, right) + legendContent.Bottom += int(tb.Height()) + right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum + legendContent.Right = util.Math.MaxInt(legendContent.Right, right) labelCount++ } } @@ -94,12 +95,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { tb := r.MeasureText(label) - ty := ycursor + tb.Height() + ty := ycursor + int(tb.Height()) r.Text(label, tx, ty) - th2 := tb.Height() >> 1 + th2 := int(tb.Height()) >> 1 - lx := tx + tb.Width() + lineTextGap + lx := tx + int(tb.Width()) + lineTextGap ly := ty - th2 lx2 := legendContent.Right - legendPadding.Right @@ -111,7 +112,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { r.LineTo(lx2, ly) r.Stroke() - ycursor += tb.Height() + ycursor += int(tb.Height()) legendCount++ } } @@ -149,7 +150,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if !s.GetStyle().Hidden { + if s.GetStyle().IsZero() || s.GetStyle().Show { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) @@ -159,12 +160,12 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { var textHeight int var textWidth int - var textBox Box + var textBox Box2d for x := 0; x < len(labels); x++ { if len(labels[x]) > 0 { textBox = r.MeasureText(labels[x]) - textHeight = MaxInt(textBox.Height(), textHeight) - textWidth = MaxInt(textBox.Width(), textWidth) + textHeight = util.Math.MaxInt(int(textBox.Height()), textHeight) + textWidth = util.Math.MaxInt(int(textBox.Width()), textWidth) } } @@ -199,7 +200,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { textBox = r.MeasureText(label) r.Text(label, tx, ty) - lx = tx + textBox.Width() + lineTextGap + lx = tx + int(textBox.Width()) + lineTextGap ly = ty - th2 r.SetStrokeColor(lines[index].GetStrokeColor()) @@ -210,7 +211,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { r.LineTo(lx+lineLengthMinimum, ly) r.Stroke() - tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum + tx += int(textBox.Width()) + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum } } } @@ -247,7 +248,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if !s.GetStyle().Hidden { + if s.GetStyle().IsZero() || s.GetStyle().Show { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) @@ -278,9 +279,9 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { if labelCount > 0 { legendContent.Bottom += DefaultMinimumTickVerticalSpacing } - legendContent.Bottom += tb.Height() - right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum - legendContent.Right = MaxInt(legendContent.Right, right) + legendContent.Bottom += int(tb.Height()) + right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum + legendContent.Right = util.Math.MaxInt(legendContent.Right, right) labelCount++ } } @@ -306,12 +307,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { tb := r.MeasureText(label) - ty := ycursor + tb.Height() + ty := ycursor + int(tb.Height()) r.Text(label, tx, ty) - th2 := tb.Height() >> 1 + th2 := int(tb.Height()) >> 1 - lx := tx + tb.Width() + lineTextGap + lx := tx + int(tb.Width()) + lineTextGap ly := ty - th2 lx2 := legendContent.Right - legendPadding.Right @@ -323,7 +324,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { r.LineTo(lx2, ly) r.Stroke() - ycursor += tb.Height() + ycursor += int(tb.Height()) legendCount++ } } diff --git a/legend_test.go b/legend_test.go index 7adbe5d..720506c 100644 --- a/legend_test.go +++ b/legend_test.go @@ -4,11 +4,11 @@ import ( "bytes" "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) func TestLegend(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) graph := Chart{ Series: []Series{ @@ -26,6 +26,6 @@ func TestLegend(t *testing.T) { } buf := bytes.NewBuffer([]byte{}) err := graph.Render(PNG, buf) - testutil.AssertNil(t, err) - testutil.AssertNotZero(t, buf.Len()) + assert.Nil(err) + assert.NotZero(buf.Len()) } diff --git a/linear_coefficient_provider.go b/linear_coefficient_provider.go deleted file mode 100644 index 9701f6b..0000000 --- a/linear_coefficient_provider.go +++ /dev/null @@ -1,42 +0,0 @@ -package chart - -// LinearCoefficientProvider is a type that returns linear cofficients. -type LinearCoefficientProvider interface { - Coefficients() (m, b, stdev, avg float64) -} - -// LinearCoefficients returns a fixed linear coefficient pair. -func LinearCoefficients(m, b float64) LinearCoefficientSet { - return LinearCoefficientSet{ - M: m, - B: b, - } -} - -// NormalizedLinearCoefficients returns a fixed linear coefficient pair. -func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet { - return LinearCoefficientSet{ - M: m, - B: b, - StdDev: stdev, - Avg: avg, - } -} - -// LinearCoefficientSet is the m and b values for the linear equation in the form: -// y = (m*x) + b -type LinearCoefficientSet struct { - M float64 - B float64 - StdDev float64 - Avg float64 -} - -// Coefficients returns the coefficients. -func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) { - m = lcs.M - b = lcs.B - stdev = lcs.StdDev - avg = lcs.Avg - return -} diff --git a/linear_regression_series.go b/linear_regression_series.go index 8ff8b1a..13c3cb0 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -2,14 +2,9 @@ package chart import ( "fmt" -) -// Interface Assertions. -var ( - _ Series = (*LinearRegressionSeries)(nil) - _ FirstValuesProvider = (*LinearRegressionSeries)(nil) - _ LastValuesProvider = (*LinearRegressionSeries)(nil) - _ LinearCoefficientProvider = (*LinearRegressionSeries)(nil) + "github.com/wcharczuk/go-chart/seq" + util "github.com/wcharczuk/go-chart/util" ) // LinearRegressionSeries is a series that plots the n-nearest neighbors @@ -29,19 +24,6 @@ type LinearRegressionSeries struct { stddevx float64 } -// Coefficients returns the linear coefficients for the series. -func (lrs LinearRegressionSeries) Coefficients() (m, b, stdev, avg float64) { - if lrs.IsZero() { - lrs.computeCoefficients() - } - - m = lrs.m - b = lrs.b - stdev = lrs.stddevx - avg = lrs.avgx - return -} - // GetName returns the name of the time series. func (lrs LinearRegressionSeries) GetName() string { return lrs.Name @@ -59,7 +41,7 @@ func (lrs LinearRegressionSeries) GetYAxis() YAxisType { // Len returns the number of elements in the series. func (lrs LinearRegressionSeries) Len() int { - return MinInt(lrs.GetLimit(), lrs.InnerSeries.Len()-lrs.GetOffset()) + return util.Math.MinInt(lrs.GetLimit(), lrs.InnerSeries.Len()-lrs.GetOffset()) } // GetLimit returns the window size. @@ -74,7 +56,7 @@ func (lrs LinearRegressionSeries) GetLimit() int { func (lrs LinearRegressionSeries) GetEndIndex() int { windowEnd := lrs.GetOffset() + lrs.GetLimit() innerSeriesLastIndex := lrs.InnerSeries.Len() - 1 - return MinInt(windowEnd, innerSeriesLastIndex) + return util.Math.MinInt(windowEnd, innerSeriesLastIndex) } // GetOffset returns the data offset. @@ -90,35 +72,22 @@ func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } - if lrs.IsZero() { + if lrs.m == 0 && lrs.b == 0 { lrs.computeCoefficients() } offset := lrs.GetOffset() - effectiveIndex := MinInt(index+offset, lrs.InnerSeries.Len()) + effectiveIndex := util.Math.MinInt(index+offset, lrs.InnerSeries.Len()) x, y = lrs.InnerSeries.GetValues(effectiveIndex) y = (lrs.m * lrs.normalize(x)) + lrs.b return } -// GetFirstValues computes the first linear regression value. -func (lrs *LinearRegressionSeries) GetFirstValues() (x, y float64) { - if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { - return - } - if lrs.IsZero() { - lrs.computeCoefficients() - } - x, y = lrs.InnerSeries.GetValues(0) - y = (lrs.m * lrs.normalize(x)) + lrs.b - return -} - // GetLastValues computes the last linear regression value. func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } - if lrs.IsZero() { + if lrs.m == 0 && lrs.b == 0 { lrs.computeCoefficients() } endIndex := lrs.GetEndIndex() @@ -127,29 +96,6 @@ func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) { return } -// Render renders the series. -func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := lrs.Style.InheritFrom(defaults) - Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs) -} - -// Validate validates the series. -func (lrs *LinearRegressionSeries) Validate() error { - if lrs.InnerSeries == nil { - return fmt.Errorf("linear regression series requires InnerSeries to be set") - } - return nil -} - -// IsZero returns if we've computed the coefficients or not. -func (lrs *LinearRegressionSeries) IsZero() bool { - return lrs.m == 0 && lrs.b == 0 -} - -// -// internal helpers -// - func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 { return (xvalue - lrs.avgx) / lrs.stddevx } @@ -161,14 +107,14 @@ func (lrs *LinearRegressionSeries) computeCoefficients() { p := float64(endIndex - startIndex) - xvalues := NewValueBufferWithCapacity(lrs.Len()) + xvalues := seq.NewBufferWithCapacity(lrs.Len()) for index := startIndex; index < endIndex; index++ { x, _ := lrs.InnerSeries.GetValues(index) xvalues.Enqueue(x) } - lrs.avgx = Seq{xvalues}.Average() - lrs.stddevx = Seq{xvalues}.StdDev() + lrs.avgx = seq.Seq{Provider: xvalues}.Average() + lrs.stddevx = seq.Seq{Provider: xvalues}.StdDev() var sumx, sumy, sumxx, sumxy float64 for index := startIndex; index < endIndex; index++ { @@ -185,3 +131,17 @@ func (lrs *LinearRegressionSeries) computeCoefficients() { lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx) lrs.b = (sumy / p) - (lrs.m * sumx / p) } + +// Render renders the series. +func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + style := lrs.Style.InheritFrom(defaults) + Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs) +} + +// Validate validates the series. +func (lrs *LinearRegressionSeries) Validate() error { + if lrs.InnerSeries == nil { + return fmt.Errorf("linear regression series requires InnerSeries to be set") + } + return nil +} diff --git a/linear_regression_series_test.go b/linear_regression_series_test.go index 0609af8..c24dff0 100644 --- a/linear_regression_series_test.go +++ b/linear_regression_series_test.go @@ -3,16 +3,17 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" ) func TestLinearRegressionSeries(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mainSeries := ContinuousSeries{ Name: "A test series", - XValues: LinearRange(1.0, 100.0), - YValues: LinearRange(1.0, 100.0), + XValues: seq.Range(1.0, 100.0), + YValues: seq.Range(1.0, 100.0), } linRegSeries := &LinearRegressionSeries{ @@ -20,21 +21,21 @@ func TestLinearRegressionSeries(t *testing.T) { } lrx0, lry0 := linRegSeries.GetValues(0) - testutil.AssertInDelta(t, 1.0, lrx0, 0.0000001) - testutil.AssertInDelta(t, 1.0, lry0, 0.0000001) + assert.InDelta(1.0, lrx0, 0.0000001) + assert.InDelta(1.0, lry0, 0.0000001) lrxn, lryn := linRegSeries.GetLastValues() - testutil.AssertInDelta(t, 100.0, lrxn, 0.0000001) - testutil.AssertInDelta(t, 100.0, lryn, 0.0000001) + assert.InDelta(100.0, lrxn, 0.0000001) + assert.InDelta(100.0, lryn, 0.0000001) } func TestLinearRegressionSeriesDesc(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mainSeries := ContinuousSeries{ Name: "A test series", - XValues: LinearRange(100.0, 1.0), - YValues: LinearRange(100.0, 1.0), + XValues: seq.Range(100.0, 1.0), + YValues: seq.Range(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ @@ -42,21 +43,21 @@ func TestLinearRegressionSeriesDesc(t *testing.T) { } lrx0, lry0 := linRegSeries.GetValues(0) - testutil.AssertInDelta(t, 100.0, lrx0, 0.0000001) - testutil.AssertInDelta(t, 100.0, lry0, 0.0000001) + assert.InDelta(100.0, lrx0, 0.0000001) + assert.InDelta(100.0, lry0, 0.0000001) lrxn, lryn := linRegSeries.GetLastValues() - testutil.AssertInDelta(t, 1.0, lrxn, 0.0000001) - testutil.AssertInDelta(t, 1.0, lryn, 0.0000001) + assert.InDelta(1.0, lrxn, 0.0000001) + assert.InDelta(1.0, lryn, 0.0000001) } func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mainSeries := ContinuousSeries{ Name: "A test series", - XValues: LinearRange(100.0, 1.0), - YValues: LinearRange(100.0, 1.0), + XValues: seq.Range(100.0, 1.0), + YValues: seq.Range(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ @@ -65,13 +66,13 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { Limit: 10, } - testutil.AssertEqual(t, 10, linRegSeries.Len()) + assert.Equal(10, linRegSeries.Len()) lrx0, lry0 := linRegSeries.GetValues(0) - testutil.AssertInDelta(t, 90.0, lrx0, 0.0000001) - testutil.AssertInDelta(t, 90.0, lry0, 0.0000001) + assert.InDelta(90.0, lrx0, 0.0000001) + assert.InDelta(90.0, lry0, 0.0000001) lrxn, lryn := linRegSeries.GetLastValues() - testutil.AssertInDelta(t, 80.0, lrxn, 0.0000001) - testutil.AssertInDelta(t, 80.0, lryn, 0.0000001) + assert.InDelta(80.0, lrxn, 0.0000001) + assert.InDelta(80.0, lryn, 0.0000001) } diff --git a/linear_sequence.go b/linear_sequence.go deleted file mode 100644 index dda761b..0000000 --- a/linear_sequence.go +++ /dev/null @@ -1,73 +0,0 @@ -package chart - -// LinearRange returns an array of values representing the range from start to end, incremented by 1.0. -func LinearRange(start, end float64) []float64 { - return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(1.0)}.Values() -} - -// LinearRangeWithStep returns the array values of a linear seq with a given start, end and optional step. -func LinearRangeWithStep(start, end, step float64) []float64 { - return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(step)}.Values() -} - -// NewLinearSequence returns a new linear generator. -func NewLinearSequence() *LinearSeq { - return &LinearSeq{step: 1.0} -} - -// LinearSeq is a stepwise generator. -type LinearSeq struct { - start float64 - end float64 - step float64 -} - -// Start returns the start value. -func (lg LinearSeq) Start() float64 { - return lg.start -} - -// End returns the end value. -func (lg LinearSeq) End() float64 { - return lg.end -} - -// Step returns the step value. -func (lg LinearSeq) Step() float64 { - return lg.step -} - -// Len returns the number of elements in the seq. -func (lg LinearSeq) Len() int { - if lg.start < lg.end { - return int((lg.end-lg.start)/lg.step) + 1 - } - return int((lg.start-lg.end)/lg.step) + 1 -} - -// GetValue returns the value at a given index. -func (lg LinearSeq) GetValue(index int) float64 { - fi := float64(index) - if lg.start < lg.end { - return lg.start + (fi * lg.step) - } - return lg.start - (fi * lg.step) -} - -// WithStart sets the start and returns the linear generator. -func (lg *LinearSeq) WithStart(start float64) *LinearSeq { - lg.start = start - return lg -} - -// WithEnd sets the end and returns the linear generator. -func (lg *LinearSeq) WithEnd(end float64) *LinearSeq { - lg.end = end - return lg -} - -// WithStep sets the step and returns the linear generator. -func (lg *LinearSeq) WithStep(step float64) *LinearSeq { - lg.step = step - return lg -} diff --git a/linear_series.go b/linear_series.go deleted file mode 100644 index 89afa93..0000000 --- a/linear_series.go +++ /dev/null @@ -1,119 +0,0 @@ -package chart - -import ( - "fmt" -) - -// Interface Assertions. -var ( - _ Series = (*LinearSeries)(nil) - _ FirstValuesProvider = (*LinearSeries)(nil) - _ LastValuesProvider = (*LinearSeries)(nil) -) - -// LinearSeries is a series that plots a line in a given domain. -type LinearSeries struct { - Name string - Style Style - YAxis YAxisType - - XValues []float64 - InnerSeries LinearCoefficientProvider - - m float64 - b float64 - stdev float64 - avg float64 -} - -// GetName returns the name of the time series. -func (ls LinearSeries) GetName() string { - return ls.Name -} - -// GetStyle returns the line style. -func (ls LinearSeries) GetStyle() Style { - return ls.Style -} - -// GetYAxis returns which YAxis the series draws on. -func (ls LinearSeries) GetYAxis() YAxisType { - return ls.YAxis -} - -// Len returns the number of elements in the series. -func (ls LinearSeries) Len() int { - return len(ls.XValues) -} - -// GetEndIndex returns the effective limit end. -func (ls LinearSeries) GetEndIndex() int { - return len(ls.XValues) - 1 -} - -// GetValues gets a value at a given index. -func (ls *LinearSeries) GetValues(index int) (x, y float64) { - if ls.InnerSeries == nil || len(ls.XValues) == 0 { - return - } - if ls.IsZero() { - ls.computeCoefficients() - } - x = ls.XValues[index] - y = (ls.m * ls.normalize(x)) + ls.b - return -} - -// GetFirstValues computes the first linear regression value. -func (ls *LinearSeries) GetFirstValues() (x, y float64) { - if ls.InnerSeries == nil || len(ls.XValues) == 0 { - return - } - if ls.IsZero() { - ls.computeCoefficients() - } - x, y = ls.GetValues(0) - return -} - -// GetLastValues computes the last linear regression value. -func (ls *LinearSeries) GetLastValues() (x, y float64) { - if ls.InnerSeries == nil || len(ls.XValues) == 0 { - return - } - if ls.IsZero() { - ls.computeCoefficients() - } - x, y = ls.GetValues(ls.GetEndIndex()) - return -} - -// Render renders the series. -func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls) -} - -// Validate validates the series. -func (ls LinearSeries) Validate() error { - if ls.InnerSeries == nil { - return fmt.Errorf("linear regression series requires InnerSeries to be set") - } - return nil -} - -// IsZero returns if the linear series has computed coefficients or not. -func (ls LinearSeries) IsZero() bool { - return ls.m == 0 && ls.b == 0 -} - -// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`. -func (ls *LinearSeries) computeCoefficients() { - ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients() -} - -func (ls *LinearSeries) normalize(xvalue float64) float64 { - if ls.avg > 0 && ls.stdev > 0 { - return (xvalue - ls.avg) / ls.stdev - } - return xvalue -} diff --git a/logarithmic_range.go b/logarithmic_range.go deleted file mode 100644 index 5b183b3..0000000 --- a/logarithmic_range.go +++ /dev/null @@ -1,94 +0,0 @@ -package chart - -import ( - "fmt" - "math" -) - -// LogarithmicRange represents a boundary for a set of numbers. -type LogarithmicRange struct { - Min float64 - Max float64 - Domain int - Descending bool -} - -// IsDescending returns if the range is descending. -func (r LogarithmicRange) IsDescending() bool { - return r.Descending -} - -// IsZero returns if the LogarithmicRange has been set or not. -func (r LogarithmicRange) IsZero() bool { - return (r.Min == 0 || math.IsNaN(r.Min)) && - (r.Max == 0 || math.IsNaN(r.Max)) && - r.Domain == 0 -} - -// GetMin gets the min value for the continuous range. -func (r LogarithmicRange) GetMin() float64 { - return r.Min -} - -// SetMin sets the min value for the continuous range. -func (r *LogarithmicRange) SetMin(min float64) { - r.Min = min -} - -// GetMax returns the max value for the continuous range. -func (r LogarithmicRange) GetMax() float64 { - return r.Max -} - -// SetMax sets the max value for the continuous range. -func (r *LogarithmicRange) SetMax(max float64) { - r.Max = max -} - -// GetDelta returns the difference between the min and max value. -func (r LogarithmicRange) GetDelta() float64 { - return r.Max - r.Min -} - -// GetDomain returns the range domain. -func (r LogarithmicRange) GetDomain() int { - return r.Domain -} - -// SetDomain sets the range domain. -func (r *LogarithmicRange) SetDomain(domain int) { - r.Domain = domain -} - -// String returns a simple string for the LogarithmicRange. -func (r LogarithmicRange) String() string { - return fmt.Sprintf("LogarithmicRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) -} - -// Translate maps a given value into the LogarithmicRange space. Modified version from ContinuousRange. -func (r LogarithmicRange) Translate(value float64) int { - if value < 1 { - return 0 - } - normalized := math.Max(value-r.Min, 1) - ratio := math.Log10(normalized) / math.Log10(r.GetDelta()) - - if r.IsDescending() { - return r.Domain - int(math.Ceil(ratio*float64(r.Domain))) - } - - return int(math.Ceil(ratio * float64(r.Domain))) -} - -// GetTicks calculates the needed ticks for the axis, in log scale. Only supports Y values > 0. -func (r LogarithmicRange) GetTicks(render Renderer, defaults Style, vf ValueFormatter) []Tick { - var ticks []Tick - exponentStart := int64(math.Max(0, math.Floor(math.Log10(r.Min)))) // one below min - exponentEnd := int64(math.Max(0, math.Ceil(math.Log10(r.Max)))) // one above max - for exp:=exponentStart; exp<=exponentEnd; exp++ { - tickVal := math.Pow(10, float64(exp)) - ticks = append(ticks, Tick{Value: tickVal, Label: vf(tickVal)}) - } - - return ticks -} diff --git a/logarithmic_range_test.go b/logarithmic_range_test.go deleted file mode 100644 index 6d3879b..0000000 --- a/logarithmic_range_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestLogRangeTranslate(t *testing.T) { - values := []float64{1, 10, 100, 1000, 10000, 100000, 1000000} - r := LogarithmicRange{Domain: 1000} - r.Min, r.Max = MinMax(values...) - - testutil.AssertEqual(t, 0, r.Translate(0)) // goes to bottom - testutil.AssertEqual(t, 0, r.Translate(1)) // goes to bottom - testutil.AssertEqual(t, 160, r.Translate(10)) // roughly 1/6th of max - testutil.AssertEqual(t, 500, r.Translate(1000)) // roughly 1/2 of max (1.0e6 / 1.0e3) - testutil.AssertEqual(t, 1000, r.Translate(1000000)) // max value -} - -func TestGetTicks(t *testing.T) { - values := []float64{35, 512, 1525122} - r := LogarithmicRange{Domain: 1000} - r.Min, r.Max = MinMax(values...) - - ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) - testutil.AssertEqual(t, 7, len(ticks)) - testutil.AssertEqual(t, 10, ticks[0].Value) - testutil.AssertEqual(t, 100, ticks[1].Value) - testutil.AssertEqual(t, 10000000, ticks[6].Value) -} - -func TestGetTicksFromHigh(t *testing.T) { - values := []float64{1412, 352144, 1525122} // min tick should be 1000 - r := LogarithmicRange{} - r.Min, r.Max = MinMax(values...) - - ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) - testutil.AssertEqual(t, 5, len(ticks)) - testutil.AssertEqual(t, float64(1000), ticks[0].Value) - testutil.AssertEqual(t, float64(10000), ticks[1].Value) - testutil.AssertEqual(t, float64(10000000), ticks[4].Value) -} diff --git a/logger.go b/logger.go deleted file mode 100644 index 28fc003..0000000 --- a/logger.go +++ /dev/null @@ -1,148 +0,0 @@ -package chart - -import ( - "fmt" - "io" - "os" - "time" -) - -var ( - _ Logger = (*StdoutLogger)(nil) -) - -// NewLogger returns a new logger. -func NewLogger(options ...LoggerOption) Logger { - stl := &StdoutLogger{ - TimeFormat: time.RFC3339Nano, - Stdout: os.Stdout, - Stderr: os.Stderr, - } - for _, option := range options { - option(stl) - } - return stl -} - -// Logger is a type that implements the logging interface. -type Logger interface { - Info(...interface{}) - Infof(string, ...interface{}) - Debug(...interface{}) - Debugf(string, ...interface{}) - Err(error) - FatalErr(error) - Error(...interface{}) - Errorf(string, ...interface{}) -} - -// Info logs an info message if the logger is set. -func Info(log Logger, arguments ...interface{}) { - if log == nil { - return - } - log.Info(arguments...) -} - -// Infof logs an info message if the logger is set. -func Infof(log Logger, format string, arguments ...interface{}) { - if log == nil { - return - } - log.Infof(format, arguments...) -} - -// Debug logs an debug message if the logger is set. -func Debug(log Logger, arguments ...interface{}) { - if log == nil { - return - } - log.Debug(arguments...) -} - -// Debugf logs an debug message if the logger is set. -func Debugf(log Logger, format string, arguments ...interface{}) { - if log == nil { - return - } - log.Debugf(format, arguments...) -} - -// LoggerOption mutates a stdout logger. -type LoggerOption = func(*StdoutLogger) - -//OptLoggerStdout sets the Stdout writer. -func OptLoggerStdout(wr io.Writer) LoggerOption { - return func(stl *StdoutLogger) { - stl.Stdout = wr - } -} - -// OptLoggerStderr sets the Stdout writer. -func OptLoggerStderr(wr io.Writer) LoggerOption { - return func(stl *StdoutLogger) { - stl.Stderr = wr - } -} - -// StdoutLogger is a basic logger. -type StdoutLogger struct { - TimeFormat string - Stdout io.Writer - Stderr io.Writer -} - -// Info writes an info message. -func (l *StdoutLogger) Info(arguments ...interface{}) { - l.Println(append([]interface{}{"[INFO]"}, arguments...)...) -} - -// Infof writes an info message. -func (l *StdoutLogger) Infof(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[INFO]"}, fmt.Sprintf(format, arguments...))...) -} - -// Debug writes an debug message. -func (l *StdoutLogger) Debug(arguments ...interface{}) { - l.Println(append([]interface{}{"[DEBUG]"}, arguments...)...) -} - -// Debugf writes an debug message. -func (l *StdoutLogger) Debugf(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[DEBUG]"}, fmt.Sprintf(format, arguments...))...) -} - -// Error writes an error message. -func (l *StdoutLogger) Error(arguments ...interface{}) { - l.Println(append([]interface{}{"[ERROR]"}, arguments...)...) -} - -// Errorf writes an error message. -func (l *StdoutLogger) Errorf(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[ERROR]"}, fmt.Sprintf(format, arguments...))...) -} - -// Err writes an error message. -func (l *StdoutLogger) Err(err error) { - if err != nil { - l.Println(append([]interface{}{"[ERROR]"}, err.Error())...) - } -} - -// FatalErr writes an error message and exits. -func (l *StdoutLogger) FatalErr(err error) { - if err != nil { - l.Println(append([]interface{}{"[FATAL]"}, err.Error())...) - os.Exit(1) - } -} - -// Println prints a new message. -func (l *StdoutLogger) Println(arguments ...interface{}) { - fmt.Fprintln(l.Stdout, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) -} - -// Errorln prints a new message. -func (l *StdoutLogger) Errorln(arguments ...interface{}) { - fmt.Fprintln(l.Stderr, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) -} diff --git a/macd_series_test.go b/macd_series_test.go index 28404b1..842eb4c 100644 --- a/macd_series_test.go +++ b/macd_series_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) var ( @@ -63,13 +63,13 @@ var ( ) func TestMACDSeries(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mockSeries := mockValuesProvider{ emaXValues, emaYValues, } - testutil.AssertEqual(t, 50, mockSeries.Len()) + assert.Equal(50, mockSeries.Len()) mas := &MACDSeries{ InnerSeries: mockSeries, @@ -81,8 +81,8 @@ func TestMACDSeries(t *testing.T) { yvalues = append(yvalues, y) } - testutil.AssertNotEmpty(t, yvalues) + assert.NotEmpty(yvalues) for index, vy := range yvalues { - testutil.AssertInDelta(t, vy, macdExpected[index], emaDelta, fmt.Sprintf("delta @ %d actual: %0.9f expected: %0.9f", index, vy, macdExpected[index])) + assert.InDelta(vy, macdExpected[index], emaDelta, fmt.Sprintf("delta @ %d actual: %0.9f expected: %0.9f", index, vy, macdExpected[index])) } } diff --git a/market_hours_range.go b/market_hours_range.go new file mode 100644 index 0000000..ebe4609 --- /dev/null +++ b/market_hours_range.go @@ -0,0 +1,194 @@ +package chart + +import ( + "fmt" + "time" + + "github.com/wcharczuk/go-chart/seq" + "github.com/wcharczuk/go-chart/util" +) + +// MarketHoursRange is a special type of range that compresses a time range into just the +// market (i.e. NYSE operating hours and days) range. +type MarketHoursRange struct { + Min time.Time + Max time.Time + + MarketOpen time.Time + MarketClose time.Time + + HolidayProvider util.HolidayProvider + + ValueFormatter ValueFormatter + + Descending bool + Domain int +} + +// IsDescending returns if the range is descending. +func (mhr MarketHoursRange) IsDescending() bool { + return mhr.Descending +} + +// GetTimezone returns the timezone for the market hours range. +func (mhr MarketHoursRange) GetTimezone() *time.Location { + return mhr.GetMarketOpen().Location() +} + +// IsZero returns if the range is setup or not. +func (mhr MarketHoursRange) IsZero() bool { + return mhr.Min.IsZero() && mhr.Max.IsZero() +} + +// GetMin returns the min value. +func (mhr MarketHoursRange) GetMin() float64 { + return util.Time.ToFloat64(mhr.Min) +} + +// GetMax returns the max value. +func (mhr MarketHoursRange) GetMax() float64 { + return util.Time.ToFloat64(mhr.GetEffectiveMax()) +} + +// GetEffectiveMax gets either the close on the max, or the max itself. +func (mhr MarketHoursRange) GetEffectiveMax() time.Time { + maxClose := util.Date.On(mhr.MarketClose, mhr.Max) + if maxClose.After(mhr.Max) { + return maxClose + } + return mhr.Max +} + +// SetMin sets the min value. +func (mhr *MarketHoursRange) SetMin(min float64) { + mhr.Min = util.Time.FromFloat64(min) + mhr.Min = mhr.Min.In(mhr.GetTimezone()) +} + +// SetMax sets the max value. +func (mhr *MarketHoursRange) SetMax(max float64) { + mhr.Max = util.Time.FromFloat64(max) + mhr.Max = mhr.Max.In(mhr.GetTimezone()) +} + +// GetDelta gets the delta. +func (mhr MarketHoursRange) GetDelta() float64 { + min := mhr.GetMin() + max := mhr.GetMax() + return max - min +} + +// GetDomain gets the domain. +func (mhr MarketHoursRange) GetDomain() int { + return mhr.Domain +} + +// SetDomain sets the domain. +func (mhr *MarketHoursRange) SetDomain(domain int) { + mhr.Domain = domain +} + +// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider. +func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider { + if mhr.HolidayProvider == nil { + return util.Date.IsNYSEHoliday + } + return mhr.HolidayProvider +} + +// GetMarketOpen returns the market open time. +func (mhr MarketHoursRange) GetMarketOpen() time.Time { + if mhr.MarketOpen.IsZero() { + return util.NYSEOpen() + } + return mhr.MarketOpen +} + +// GetMarketClose returns the market close time. +func (mhr MarketHoursRange) GetMarketClose() time.Time { + if mhr.MarketClose.IsZero() { + return util.NYSEClose() + } + return mhr.MarketClose +} + +// GetTicks returns the ticks for the range. +// This is to override the default continous ticks that would be generated for the range. +func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick { + times := seq.TimeUtil.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth := mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + + times = seq.TimeUtil.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth = mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + + times = seq.TimeUtil.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth = mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + + times = seq.TimeUtil.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth = mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + + times = seq.TimeUtil.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth = mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + + return GenerateContinuousTicks(r, mhr, false, defaults, vf) +} + +func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int { + defaults.GetTextOptions().WriteToRenderer(r) + var total int + for index, t := range times { + timeLabel := vf(t) + + labelBox := r.MeasureText(timeLabel) + total += int(labelBox.Width()) + if index > 0 { + total += DefaultMinimumTickHorizontalSpacing + } + } + return total +} + +func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []Tick { + ticks := make([]Tick, len(times)) + for index, t := range times { + ticks[index] = Tick{ + Value: util.Time.ToFloat64(t), + Label: vf(t), + } + } + return ticks +} + +func (mhr MarketHoursRange) String() string { + return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(time.RFC3339), mhr.Max.Format(time.RFC3339), mhr.Domain) +} + +// Translate maps a given value into the ContinuousRange space. +func (mhr MarketHoursRange) Translate(value float64) int { + valueTime := util.Time.FromFloat64(value) + valueTimeEastern := valueTime.In(util.Date.Eastern()) + totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain)) + + if mhr.IsDescending() { + return mhr.Domain - translated + } + + return translated +} diff --git a/market_hours_range_test.go b/market_hours_range_test.go new file mode 100644 index 0000000..3846ccc --- /dev/null +++ b/market_hours_range_test.go @@ -0,0 +1,73 @@ +package chart + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" +) + +func TestMarketHoursRangeGetDelta(t *testing.T) { + assert := assert.New(t) + + r := &MarketHoursRange{ + Min: time.Date(2016, 07, 19, 9, 30, 0, 0, util.Date.Eastern()), + Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()), + MarketOpen: util.NYSEOpen(), + MarketClose: util.NYSEClose(), + HolidayProvider: util.Date.IsNYSEHoliday, + } + + assert.NotZero(r.GetDelta()) +} + +func TestMarketHoursRangeTranslate(t *testing.T) { + assert := assert.New(t) + + r := &MarketHoursRange{ + Min: time.Date(2016, 07, 18, 9, 30, 0, 0, util.Date.Eastern()), + Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()), + MarketOpen: util.NYSEOpen(), + MarketClose: util.NYSEClose(), + HolidayProvider: util.Date.IsNYSEHoliday, + Domain: 1000, + } + + weds := time.Date(2016, 07, 20, 9, 30, 0, 0, util.Date.Eastern()) + + assert.Equal(0, r.Translate(util.Time.ToFloat64(r.Min))) + assert.Equal(400, r.Translate(util.Time.ToFloat64(weds))) + assert.Equal(1000, r.Translate(util.Time.ToFloat64(r.Max))) +} + +func TestMarketHoursRangeGetTicks(t *testing.T) { + assert := assert.New(t) + + r, err := PNG(1024, 1024) + assert.Nil(err) + + f, err := GetDefaultFont() + assert.Nil(err) + + defaults := Style{ + Font: f, + FontSize: 10, + FontColor: ColorBlack, + } + + ra := &MarketHoursRange{ + Min: util.Date.On(util.NYSEOpen(), util.Date.Date(2016, 07, 18, util.Date.Eastern())), + Max: util.Date.On(util.NYSEClose(), util.Date.Date(2016, 07, 22, util.Date.Eastern())), + MarketOpen: util.NYSEOpen(), + MarketClose: util.NYSEClose(), + HolidayProvider: util.Date.IsNYSEHoliday, + Domain: 1024, + } + + ticks := ra.GetTicks(r, defaults, TimeValueFormatter) + assert.NotEmpty(ticks) + assert.Len(ticks, 5) + assert.NotEqual(util.Time.ToFloat64(ra.Min), ticks[0].Value) + assert.NotEmpty(ticks[0].Label) +} diff --git a/matrix/matrix_test.go b/matrix/matrix_test.go index 3a73cec..bc896be 100644 --- a/matrix/matrix_test.go +++ b/matrix/matrix_test.go @@ -3,139 +3,139 @@ package matrix import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestNew(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := New(10, 5) rows, cols := m.Size() - testutil.AssertEqual(t, 10, rows) - testutil.AssertEqual(t, 5, cols) - testutil.AssertZero(t, m.Get(0, 0)) - testutil.AssertZero(t, m.Get(9, 4)) + assert.Equal(10, rows) + assert.Equal(5, cols) + assert.Zero(m.Get(0, 0)) + assert.Zero(m.Get(9, 4)) } func TestNewWithValues(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := New(5, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) rows, cols := m.Size() - testutil.AssertEqual(t, 5, rows) - testutil.AssertEqual(t, 2, cols) - testutil.AssertEqual(t, 1, m.Get(0, 0)) - testutil.AssertEqual(t, 10, m.Get(4, 1)) + assert.Equal(5, rows) + assert.Equal(2, cols) + assert.Equal(1, m.Get(0, 0)) + assert.Equal(10, m.Get(4, 1)) } func TestIdentitiy(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) id := Identity(5) rows, cols := id.Size() - testutil.AssertEqual(t, 5, rows) - testutil.AssertEqual(t, 5, cols) - testutil.AssertEqual(t, 1, id.Get(0, 0)) - testutil.AssertEqual(t, 1, id.Get(1, 1)) - testutil.AssertEqual(t, 1, id.Get(2, 2)) - testutil.AssertEqual(t, 1, id.Get(3, 3)) - testutil.AssertEqual(t, 1, id.Get(4, 4)) - testutil.AssertEqual(t, 0, id.Get(0, 1)) - testutil.AssertEqual(t, 0, id.Get(1, 0)) - testutil.AssertEqual(t, 0, id.Get(4, 0)) - testutil.AssertEqual(t, 0, id.Get(0, 4)) + assert.Equal(5, rows) + assert.Equal(5, cols) + assert.Equal(1, id.Get(0, 0)) + assert.Equal(1, id.Get(1, 1)) + assert.Equal(1, id.Get(2, 2)) + assert.Equal(1, id.Get(3, 3)) + assert.Equal(1, id.Get(4, 4)) + assert.Equal(0, id.Get(0, 1)) + assert.Equal(0, id.Get(1, 0)) + assert.Equal(0, id.Get(4, 0)) + assert.Equal(0, id.Get(0, 4)) } func TestNewFromArrays(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3, 4}, {5, 6, 7, 8}, }) - testutil.AssertNotNil(t, m) + assert.NotNil(m) rows, cols := m.Size() - testutil.AssertEqual(t, 2, rows) - testutil.AssertEqual(t, 4, cols) + assert.Equal(2, rows) + assert.Equal(4, cols) } func TestOnes(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) ones := Ones(5, 10) rows, cols := ones.Size() - testutil.AssertEqual(t, 5, rows) - testutil.AssertEqual(t, 10, cols) + assert.Equal(5, rows) + assert.Equal(10, cols) for row := 0; row < rows; row++ { for col := 0; col < cols; col++ { - testutil.AssertEqual(t, 1, ones.Get(row, col)) + assert.Equal(1, ones.Get(row, col)) } } } func TestMatrixEpsilon(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) ones := Ones(2, 2) ones = ones.WithEpsilon(0.001) - testutil.AssertEqual(t, 0.001, ones.Epsilon()) + assert.Equal(0.001, ones.Epsilon()) } func TestMatrixArrays(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, {4, 5, 6}, }) - testutil.AssertNotNil(t, m) + assert.NotNil(m) arrays := m.Arrays() - testutil.AssertEqual(t, arrays, [][]float64{ + assert.Equal(arrays, [][]float64{ {1, 2, 3}, {4, 5, 6}, }) } func TestMatrixIsSquare(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) - testutil.AssertFalse(t, NewFromArrays([][]float64{ + assert.False(NewFromArrays([][]float64{ {1, 2, 3}, {4, 5, 6}, }).IsSquare()) - testutil.AssertFalse(t, NewFromArrays([][]float64{ + assert.False(NewFromArrays([][]float64{ {1, 2}, {3, 4}, {5, 6}, }).IsSquare()) - testutil.AssertTrue(t, NewFromArrays([][]float64{ + assert.True(NewFromArrays([][]float64{ {1, 2}, {3, 4}, }).IsSquare()) } func TestMatrixIsSymmetric(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) - testutil.AssertFalse(t, NewFromArrays([][]float64{ + assert.False(NewFromArrays([][]float64{ {1, 2, 3}, {2, 1, 2}, }).IsSymmetric()) - testutil.AssertFalse(t, NewFromArrays([][]float64{ + assert.False(NewFromArrays([][]float64{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, }).IsSymmetric()) - testutil.AssertTrue(t, NewFromArrays([][]float64{ + assert.True(NewFromArrays([][]float64{ {1, 2, 3}, {2, 1, 2}, {3, 2, 1}, @@ -144,7 +144,7 @@ func TestMatrixIsSymmetric(t *testing.T) { } func TestMatrixGet(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -152,19 +152,19 @@ func TestMatrixGet(t *testing.T) { {7, 8, 9}, }) - testutil.AssertEqual(t, 1, m.Get(0, 0)) - testutil.AssertEqual(t, 2, m.Get(0, 1)) - testutil.AssertEqual(t, 3, m.Get(0, 2)) - testutil.AssertEqual(t, 4, m.Get(1, 0)) - testutil.AssertEqual(t, 5, m.Get(1, 1)) - testutil.AssertEqual(t, 6, m.Get(1, 2)) - testutil.AssertEqual(t, 7, m.Get(2, 0)) - testutil.AssertEqual(t, 8, m.Get(2, 1)) - testutil.AssertEqual(t, 9, m.Get(2, 2)) + assert.Equal(1, m.Get(0, 0)) + assert.Equal(2, m.Get(0, 1)) + assert.Equal(3, m.Get(0, 2)) + assert.Equal(4, m.Get(1, 0)) + assert.Equal(5, m.Get(1, 1)) + assert.Equal(6, m.Get(1, 2)) + assert.Equal(7, m.Get(2, 0)) + assert.Equal(8, m.Get(2, 1)) + assert.Equal(9, m.Get(2, 2)) } func TestMatrixSet(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -173,11 +173,11 @@ func TestMatrixSet(t *testing.T) { }) m.Set(1, 1, 99) - testutil.AssertEqual(t, 99, m.Get(1, 1)) + assert.Equal(99, m.Get(1, 1)) } func TestMatrixCol(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -185,13 +185,13 @@ func TestMatrixCol(t *testing.T) { {7, 8, 9}, }) - testutil.AssertEqual(t, []float64{1, 4, 7}, m.Col(0)) - testutil.AssertEqual(t, []float64{2, 5, 8}, m.Col(1)) - testutil.AssertEqual(t, []float64{3, 6, 9}, m.Col(2)) + assert.Equal([]float64{1, 4, 7}, m.Col(0)) + assert.Equal([]float64{2, 5, 8}, m.Col(1)) + assert.Equal([]float64{3, 6, 9}, m.Col(2)) } func TestMatrixRow(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -199,13 +199,13 @@ func TestMatrixRow(t *testing.T) { {7, 8, 9}, }) - testutil.AssertEqual(t, []float64{1, 2, 3}, m.Row(0)) - testutil.AssertEqual(t, []float64{4, 5, 6}, m.Row(1)) - testutil.AssertEqual(t, []float64{7, 8, 9}, m.Row(2)) + assert.Equal([]float64{1, 2, 3}, m.Row(0)) + assert.Equal([]float64{4, 5, 6}, m.Row(1)) + assert.Equal([]float64{7, 8, 9}, m.Row(2)) } func TestMatrixSwapRows(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -215,13 +215,13 @@ func TestMatrixSwapRows(t *testing.T) { m.SwapRows(0, 1) - testutil.AssertEqual(t, []float64{4, 5, 6}, m.Row(0)) - testutil.AssertEqual(t, []float64{1, 2, 3}, m.Row(1)) - testutil.AssertEqual(t, []float64{7, 8, 9}, m.Row(2)) + assert.Equal([]float64{4, 5, 6}, m.Row(0)) + assert.Equal([]float64{1, 2, 3}, m.Row(1)) + assert.Equal([]float64{7, 8, 9}, m.Row(2)) } func TestMatrixCopy(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -230,12 +230,12 @@ func TestMatrixCopy(t *testing.T) { }) m2 := m.Copy() - testutil.AssertFalse(t, m == m2) - testutil.AssertTrue(t, m.Equals(m2)) + assert.False(m == m2) + assert.True(m.Equals(m2)) } func TestMatrixDiagonalVector(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 4, 7}, @@ -244,11 +244,11 @@ func TestMatrixDiagonalVector(t *testing.T) { }) diag := m.DiagonalVector() - testutil.AssertEqual(t, []float64{1, 2, 3}, diag) + assert.Equal([]float64{1, 2, 3}, diag) } func TestMatrixDiagonalVectorLandscape(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 4, 7, 99}, @@ -256,11 +256,11 @@ func TestMatrixDiagonalVectorLandscape(t *testing.T) { }) diag := m.DiagonalVector() - testutil.AssertEqual(t, []float64{1, 2}, diag) + assert.Equal([]float64{1, 2}, diag) } func TestMatrixDiagonalVectorPortrait(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 4}, @@ -269,11 +269,11 @@ func TestMatrixDiagonalVectorPortrait(t *testing.T) { }) diag := m.DiagonalVector() - testutil.AssertEqual(t, []float64{1, 2}, diag) + assert.Equal([]float64{1, 2}, diag) } func TestMatrixDiagonal(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 4, 7}, @@ -287,11 +287,11 @@ func TestMatrixDiagonal(t *testing.T) { {0, 0, 3}, }) - testutil.AssertTrue(t, m.Diagonal().Equals(m2)) + assert.True(m.Diagonal().Equals(m2)) } func TestMatrixEquals(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 4, 7}, @@ -299,16 +299,16 @@ func TestMatrixEquals(t *testing.T) { {7, 8, 3}, }) - testutil.AssertFalse(t, m.Equals(nil)) + assert.False(m.Equals(nil)) var nilMatrix *Matrix - testutil.AssertTrue(t, nilMatrix.Equals(nil)) - testutil.AssertFalse(t, m.Equals(New(1, 1))) - testutil.AssertFalse(t, m.Equals(New(3, 3))) - testutil.AssertTrue(t, m.Equals(New(3, 3, 1, 4, 7, 4, 2, 8, 7, 8, 3))) + assert.True(nilMatrix.Equals(nil)) + assert.False(m.Equals(New(1, 1))) + assert.False(m.Equals(New(3, 3))) + assert.True(m.Equals(New(3, 3, 1, 4, 7, 4, 2, 8, 7, 8, 3))) } func TestMatrixL(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -317,11 +317,11 @@ func TestMatrixL(t *testing.T) { }) l := m.L() - testutil.AssertTrue(t, l.Equals(New(3, 3, 1, 2, 3, 0, 5, 6, 0, 0, 9))) + assert.True(l.Equals(New(3, 3, 1, 2, 3, 0, 5, 6, 0, 0, 9))) } func TestMatrixU(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -330,11 +330,11 @@ func TestMatrixU(t *testing.T) { }) u := m.U() - testutil.AssertTrue(t, u.Equals(New(3, 3, 0, 0, 0, 4, 0, 0, 7, 8, 0))) + assert.True(u.Equals(New(3, 3, 0, 0, 0, 4, 0, 0, 7, 8, 0))) } func TestMatrixString(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -342,11 +342,11 @@ func TestMatrixString(t *testing.T) { {7, 8, 9}, }) - testutil.AssertEqual(t, "1 2 3 \n4 5 6 \n7 8 9 \n", m.String()) + assert.Equal("1 2 3 \n4 5 6 \n7 8 9 \n", m.String()) } func TestMatrixLU(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 3, 5}, @@ -355,13 +355,13 @@ func TestMatrixLU(t *testing.T) { }) l, u, p := m.LU() - testutil.AssertNotNil(t, l) - testutil.AssertNotNil(t, u) - testutil.AssertNotNil(t, p) + assert.NotNil(l) + assert.NotNil(u) + assert.NotNil(p) } func TestMatrixQR(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {12, -51, 4}, @@ -370,12 +370,12 @@ func TestMatrixQR(t *testing.T) { }) q, r := m.QR() - testutil.AssertNotNil(t, q) - testutil.AssertNotNil(t, r) + assert.NotNil(q) + assert.NotNil(r) } func TestMatrixTranspose(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) m := NewFromArrays([][]float64{ {1, 2, 3}, @@ -387,10 +387,10 @@ func TestMatrixTranspose(t *testing.T) { m2 := m.Transpose() rows, cols := m2.Size() - testutil.AssertEqual(t, 3, rows) - testutil.AssertEqual(t, 4, cols) + assert.Equal(3, rows) + assert.Equal(4, cols) - testutil.AssertEqual(t, 1, m2.Get(0, 0)) - testutil.AssertEqual(t, 10, m2.Get(0, 3)) - testutil.AssertEqual(t, 3, m2.Get(2, 0)) + assert.Equal(1, m2.Get(0, 0)) + assert.Equal(10, m2.Get(0, 3)) + assert.Equal(3, m2.Get(2, 0)) } diff --git a/matrix/regression_test.go b/matrix/regression_test.go index 07c868d..c55a480 100644 --- a/matrix/regression_test.go +++ b/matrix/regression_test.go @@ -3,20 +3,20 @@ package matrix import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestPoly(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) var xGiven = []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var yGiven = []float64{1, 6, 17, 34, 57, 86, 121, 162, 209, 262, 321} var degree = 2 c, err := Poly(xGiven, yGiven, degree) - testutil.AssertNil(t, err) - testutil.AssertLen(t, c, 3) + assert.Nil(err) + assert.Len(c, 3) - testutil.AssertInDelta(t, c[0], 0.999999999, DefaultEpsilon) - testutil.AssertInDelta(t, c[1], 2, DefaultEpsilon) - testutil.AssertInDelta(t, c[2], 3, DefaultEpsilon) + assert.InDelta(c[0], 0.999999999, DefaultEpsilon) + assert.InDelta(c[1], 2, DefaultEpsilon) + assert.InDelta(c[2], 3, DefaultEpsilon) } diff --git a/parse.go b/parse.go deleted file mode 100644 index 5ecae0a..0000000 --- a/parse.go +++ /dev/null @@ -1,40 +0,0 @@ -package chart - -import ( - "strconv" - "strings" - "time" -) - -// ParseFloats parses a list of floats. -func ParseFloats(values ...string) ([]float64, error) { - var output []float64 - var parsedValue float64 - var err error - var cleaned string - for _, value := range values { - cleaned = strings.TrimSpace(strings.Replace(value, ",", "", -1)) - if cleaned == "" { - continue - } - if parsedValue, err = strconv.ParseFloat(cleaned, 64); err != nil { - return nil, err - } - output = append(output, parsedValue) - } - return output, nil -} - -// ParseTimes parses a list of times with a given format. -func ParseTimes(layout string, values ...string) ([]time.Time, error) { - var output []time.Time - var parsedValue time.Time - var err error - for _, value := range values { - if parsedValue, err = time.Parse(layout, value); err != nil { - return nil, err - } - output = append(output, parsedValue) - } - return output, nil -} diff --git a/percent_change_series.go b/percent_change_series.go deleted file mode 100644 index 3767893..0000000 --- a/percent_change_series.go +++ /dev/null @@ -1,89 +0,0 @@ -package chart - -// Interface Assertions. -var ( - _ Series = (*PercentChangeSeries)(nil) - _ FirstValuesProvider = (*PercentChangeSeries)(nil) - _ LastValuesProvider = (*PercentChangeSeries)(nil) - _ ValueFormatterProvider = (*PercentChangeSeries)(nil) -) - -// PercentChangeSeriesSource is a series that -// can be used with a PercentChangeSeries -type PercentChangeSeriesSource interface { - Series - FirstValuesProvider - LastValuesProvider - ValuesProvider - ValueFormatterProvider -} - -// PercentChangeSeries applies a -// percentage difference function to a given continuous series. -type PercentChangeSeries struct { - Name string - Style Style - YAxis YAxisType - InnerSeries PercentChangeSeriesSource -} - -// GetName returns the name of the time series. -func (pcs PercentChangeSeries) GetName() string { - return pcs.Name -} - -// GetStyle returns the line style. -func (pcs PercentChangeSeries) GetStyle() Style { - return pcs.Style -} - -// Len implements part of Series. -func (pcs PercentChangeSeries) Len() int { - return pcs.InnerSeries.Len() -} - -// GetFirstValues implements FirstValuesProvider. -func (pcs PercentChangeSeries) GetFirstValues() (x, y float64) { - return pcs.InnerSeries.GetFirstValues() -} - -// GetValues gets x, y values at a given index. -func (pcs PercentChangeSeries) GetValues(index int) (x, y float64) { - _, fy := pcs.InnerSeries.GetFirstValues() - x0, y0 := pcs.InnerSeries.GetValues(index) - x = x0 - y = PercentDifference(fy, y0) - return -} - -// GetValueFormatters returns value formatter defaults for the series. -func (pcs PercentChangeSeries) GetValueFormatters() (x, y ValueFormatter) { - x, _ = pcs.InnerSeries.GetValueFormatters() - y = PercentValueFormatter - return -} - -// GetYAxis returns which YAxis the series draws on. -func (pcs PercentChangeSeries) GetYAxis() YAxisType { - return pcs.YAxis -} - -// GetLastValues gets the last values. -func (pcs PercentChangeSeries) GetLastValues() (x, y float64) { - _, fy := pcs.InnerSeries.GetFirstValues() - x0, y0 := pcs.InnerSeries.GetLastValues() - x = x0 - y = PercentDifference(fy, y0) - return -} - -// Render renders the series. -func (pcs PercentChangeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := pcs.Style.InheritFrom(defaults) - Draw.LineSeries(r, canvasBox, xrange, yrange, style, pcs) -} - -// Validate validates the series. -func (pcs PercentChangeSeries) Validate() error { - return pcs.InnerSeries.Validate() -} diff --git a/percent_change_series_test.go b/percent_change_series_test.go deleted file mode 100644 index c80d370..0000000 --- a/percent_change_series_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestPercentageDifferenceSeries(t *testing.T) { - // replaced new assertions helper - - cs := ContinuousSeries{ - XValues: LinearRange(1.0, 10.0), - YValues: LinearRange(1.0, 10.0), - } - - pcs := PercentChangeSeries{ - Name: "Test Series", - InnerSeries: cs, - } - - testutil.AssertEqual(t, "Test Series", pcs.GetName()) - testutil.AssertEqual(t, 10, pcs.Len()) - x0, y0 := pcs.GetValues(0) - testutil.AssertEqual(t, 1.0, x0) - testutil.AssertEqual(t, 0, y0) - - xn, yn := pcs.GetValues(9) - testutil.AssertEqual(t, 10.0, xn) - testutil.AssertEqual(t, 9.0, yn) - - xn, yn = pcs.GetLastValues() - testutil.AssertEqual(t, 10.0, xn) - testutil.AssertEqual(t, 9.0, yn) -} diff --git a/pie_chart.go b/pie_chart.go index 49b551f..3a9a523 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -4,8 +4,15 @@ import ( "errors" "fmt" "io" + "math" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/util" +) + +const ( + _pi2 = math.Pi / 2.0 + _pi4 = math.Pi / 4.0 ) // PieChart is a chart that draws sections of a circle based on percentages. @@ -116,40 +123,33 @@ func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) { } func (pc PieChart) drawTitle(r Renderer) { - if len(pc.Title) > 0 && !pc.TitleStyle.Hidden { + if len(pc.Title) > 0 && pc.TitleStyle.Show { Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle()) } } func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) { cx, cy := canvasBox.Center() - diameter := MinInt(canvasBox.Width(), canvasBox.Height()) + diameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height()) radius := float64(diameter >> 1) labelRadius := (radius * 2.0) / 3.0 // draw the pie slices var rads, delta, delta2, total float64 var lx, ly int + for index, v := range values { + v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r) - if len(values) == 1 { - pc.stylePieChartValue(0).WriteToRenderer(r) r.MoveTo(cx, cy) - r.Circle(radius, cx, cy) - } else { - for index, v := range values { - v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r) + rads = util.Math.PercentToRadians(total) + delta = util.Math.PercentToRadians(v.Value) - r.MoveTo(cx, cy) - rads = PercentToRadians(total) - delta = PercentToRadians(v.Value) + r.ArcTo(cx, cy, radius, radius, rads, delta) - r.ArcTo(cx, cy, radius, radius, rads, delta) - - r.LineTo(cx, cy) - r.Close() - r.FillStroke() - total = total + v.Value - } + r.LineTo(cx, cy) + r.Close() + r.FillStroke() + total = total + v.Value } // draw the labels @@ -157,20 +157,13 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) { for index, v := range values { v.Style.InheritFrom(pc.stylePieChartValue(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) + delta2 = util.Math.PercentToRadians(total + (v.Value / 2.0)) + delta2 = util.Math.RadianAdd(delta2, _pi2) + lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2) tb := r.MeasureText(v.Label) - lx = lx - (tb.Width() >> 1) - ly = ly + (tb.Height() >> 1) - - if lx < 0 { - lx = 0 - } - if ly < 0 { - lx = 0 - } + lx = lx - (int(tb.Width()) >> 1) + ly = ly + (int(tb.Height()) >> 1) r.Text(v.Label, lx, ly) } @@ -191,7 +184,7 @@ func (pc PieChart) getDefaultCanvasBox() Box { } func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box { - circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height()) + circleDiameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height()) square := Box{ Right: circleDiameter, @@ -237,7 +230,7 @@ func (pc PieChart) stylePieChartValue(index int) Style { } func (pc PieChart) getScaledFontSize() float64 { - effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight()) + effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight()) if effectiveDimension >= 2048 { return 48.0 } else if effectiveDimension >= 1024 { @@ -276,7 +269,7 @@ func (pc PieChart) styleDefaultsTitle() Style { } func (pc PieChart) getTitleFontSize() float64 { - effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight()) + effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight()) if effectiveDimension >= 2048 { return 48 } else if effectiveDimension >= 1024 { diff --git a/pie_chart_test.go b/pie_chart_test.go index 50de754..788cbe2 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -4,11 +4,11 @@ import ( "bytes" "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestPieChart(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) pie := PieChart{ Canvas: Style{ @@ -27,11 +27,11 @@ func TestPieChart(t *testing.T) { b := bytes.NewBuffer([]byte{}) pie.Render(PNG, b) - testutil.AssertNotZero(t, b.Len()) + assert.NotZero(b.Len()) } func TestPieChartDropsZeroValues(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) pie := PieChart{ Canvas: Style{ @@ -46,11 +46,11 @@ func TestPieChartDropsZeroValues(t *testing.T) { b := bytes.NewBuffer([]byte{}) err := pie.Render(PNG, b) - testutil.AssertNil(t, err) + assert.Nil(err) } func TestPieChartAllZeroValues(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) pie := PieChart{ Canvas: Style{ @@ -65,5 +65,5 @@ func TestPieChartAllZeroValues(t *testing.T) { b := bytes.NewBuffer([]byte{}) err := pie.Render(PNG, b) - testutil.AssertNotNil(t, err) + assert.NotNil(err) } diff --git a/polynomial_regression_series.go b/polynomial_regression_series.go index 1187192..506a4cb 100644 --- a/polynomial_regression_series.go +++ b/polynomial_regression_series.go @@ -4,14 +4,8 @@ import ( "fmt" "math" - "git.smarteching.com/zeni/go-chart/v2/matrix" -) - -// Interface Assertions. -var ( - _ Series = (*PolynomialRegressionSeries)(nil) - _ FirstValuesProvider = (*PolynomialRegressionSeries)(nil) - _ LastValuesProvider = (*PolynomialRegressionSeries)(nil) + "github.com/wcharczuk/go-chart/matrix" + util "github.com/wcharczuk/go-chart/util" ) // PolynomialRegressionSeries implements a polynomial regression over a given @@ -46,7 +40,7 @@ func (prs PolynomialRegressionSeries) GetYAxis() YAxisType { // Len returns the number of elements in the series. func (prs PolynomialRegressionSeries) Len() int { - return MinInt(prs.GetLimit(), prs.InnerSeries.Len()-prs.GetOffset()) + return util.Math.MinInt(prs.GetLimit(), prs.InnerSeries.Len()-prs.GetOffset()) } // GetLimit returns the window size. @@ -61,7 +55,7 @@ func (prs PolynomialRegressionSeries) GetLimit() int { func (prs PolynomialRegressionSeries) GetEndIndex() int { windowEnd := prs.GetOffset() + prs.GetLimit() innerSeriesLastIndex := prs.InnerSeries.Len() - 1 - return MinInt(windowEnd, innerSeriesLastIndex) + return util.Math.MinInt(windowEnd, innerSeriesLastIndex) } // GetOffset returns the data offset. @@ -101,29 +95,12 @@ func (prs *PolynomialRegressionSeries) GetValues(index int) (x, y float64) { } offset := prs.GetOffset() - effectiveIndex := MinInt(index+offset, prs.InnerSeries.Len()) + effectiveIndex := util.Math.MinInt(index+offset, prs.InnerSeries.Len()) x, y = prs.InnerSeries.GetValues(effectiveIndex) y = prs.apply(x) return } -// GetFirstValues computes the first poly regression value. -func (prs *PolynomialRegressionSeries) GetFirstValues() (x, y float64) { - if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 { - return - } - if prs.coeffs == nil { - coeffs, err := prs.computeCoefficients() - if err != nil { - panic(err) - } - prs.coeffs = coeffs - } - x, y = prs.InnerSeries.GetValues(0) - y = prs.apply(x) - return -} - // GetLastValues computes the last poly regression value. func (prs *PolynomialRegressionSeries) GetLastValues() (x, y float64) { if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 { diff --git a/polynomial_regression_test.go b/polynomial_regression_test.go index 54bd655..beabf37 100644 --- a/polynomial_regression_test.go +++ b/polynomial_regression_test.go @@ -3,12 +3,12 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/matrix" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/matrix" ) func TestPolynomialRegression(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) var xv []float64 var yv []float64 @@ -30,6 +30,6 @@ func TestPolynomialRegression(t *testing.T) { for i := 0; i < 100; i++ { _, y := poly.GetValues(i) - testutil.AssertInDelta(t, float64(i*i), y, matrix.DefaultEpsilon) + assert.InDelta(float64(i*i), y, matrix.DefaultEpsilon) } } diff --git a/raster_renderer.go b/raster_renderer.go index d209ca9..10be4b4 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -6,8 +6,9 @@ import ( "io" "math" - "git.smarteching.com/zeni/go-chart/v2/drawing" + util "github.com/blendlabs/go-util" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" ) // PNG returns a new png/raster renderer. @@ -48,9 +49,6 @@ func (rr *rasterRenderer) SetDPI(dpi float64) { rr.gc.SetDPI(dpi) } -// SetClassName implements the interface method. However, PNGs have no classes. -func (rr *rasterRenderer) SetClassName(_ string) {} - // SetStrokeColor implements the interface method. func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) { rr.s.StrokeColor = c @@ -157,13 +155,13 @@ func (rr *rasterRenderer) Text(body string, x, y int) { } // MeasureText returns the height and width in pixels of a string. -func (rr *rasterRenderer) MeasureText(body string) Box { +func (rr *rasterRenderer) MeasureText(body string) Box2d { rr.gc.SetFont(rr.s.Font) rr.gc.SetFontSize(rr.s.FontSize) rr.gc.SetFillColor(rr.s.FontColor) l, t, r, b, err := rr.gc.GetStringBounds(body) if err != nil { - return Box{} + return Box2d{} } if l < 0 { r = r - l // equivalent to r+(-1*l) @@ -191,10 +189,10 @@ func (rr *rasterRenderer) MeasureText(body string) Box { Bottom: int(math.Ceil(b)), } if rr.rotateRadians == nil { - return textBox + return textBox.Corners() } - return textBox.Corners().Rotate(RadiansToDegrees(*rr.rotateRadians)).Box() + return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians)) } // SetTextRotation sets a text rotation. diff --git a/renderer.go b/renderer.go index b16f5e4..dc1949d 100644 --- a/renderer.go +++ b/renderer.go @@ -3,8 +3,8 @@ package chart import ( "io" - "git.smarteching.com/zeni/go-chart/v2/drawing" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" ) // Renderer represents the basic methods required to draw a chart. @@ -18,9 +18,6 @@ type Renderer interface { // SetDPI sets the DPI for the renderer. SetDPI(dpi float64) - // SetClassName sets the current class name. - SetClassName(string) - // SetStrokeColor sets the current stroke color. SetStrokeColor(drawing.Color) @@ -76,7 +73,7 @@ type Renderer interface { Text(body string, x, y int) // MeasureText measures text. - MeasureText(body string) Box + MeasureText(body string) Box2d // SetTextRotatation sets a rotation for drawing elements. SetTextRotation(radians float64) diff --git a/seq/array.go b/seq/array.go new file mode 100644 index 0000000..01ca630 --- /dev/null +++ b/seq/array.go @@ -0,0 +1,34 @@ +package seq + +import "time" + +// NewArray creates a new array. +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] +} + +// ArrayOfTimes wraps an array of times as a sequence provider. +type ArrayOfTimes []time.Time + +// Len returns the length of the array. +func (aot ArrayOfTimes) Len() int { + return len(aot) +} + +// GetValue returns the time at the given index as a time.Time. +func (aot ArrayOfTimes) GetValue(index int) time.Time { + return aot[index] +} diff --git a/value_buffer.go b/seq/buffer.go similarity index 79% rename from value_buffer.go rename to seq/buffer.go index d544bd3..be7c32e 100644 --- a/value_buffer.go +++ b/seq/buffer.go @@ -1,8 +1,10 @@ -package chart +package seq import ( "fmt" "strings" + + util "github.com/wcharczuk/go-chart/util" ) const ( @@ -12,15 +14,19 @@ const ( bufferDefaultCapacity = 4 ) -// NewValueBuffer creates a new value buffer with an optional set of values. -func NewValueBuffer(values ...float64) *ValueBuffer { +var ( + emptyArray = make([]float64, 0) +) + +// NewBuffer creates a new value buffer with an optional set of values. +func NewBuffer(values ...float64) *Buffer { var tail int - array := make([]float64, MaxInt(len(values), bufferDefaultCapacity)) + array := make([]float64, util.Math.MaxInt(len(values), bufferDefaultCapacity)) if len(values) > 0 { copy(array, values) tail = len(values) } - return &ValueBuffer{ + return &Buffer{ array: array, head: 0, tail: tail, @@ -28,9 +34,9 @@ func NewValueBuffer(values ...float64) *ValueBuffer { } } -// NewValueBufferWithCapacity creates a new ValueBuffer pre-allocated with the given capacity. -func NewValueBufferWithCapacity(capacity int) *ValueBuffer { - return &ValueBuffer{ +// NewBufferWithCapacity creates a new ValueBuffer pre-allocated with the given capacity. +func NewBufferWithCapacity(capacity int) *Buffer { + return &Buffer{ array: make([]float64, capacity), head: 0, tail: 0, @@ -38,11 +44,11 @@ func NewValueBufferWithCapacity(capacity int) *ValueBuffer { } } -// ValueBuffer is a fifo datastructure that is backed by a pre-allocated array. +// Buffer is a fifo datastructure that is backed by a pre-allocated array. // Instead of allocating a whole new node object for each element, array elements are re-used (which saves GC churn). // Enqueue can be O(n), Dequeue is generally O(1). // Buffer implements `seq.Provider` -type ValueBuffer struct { +type Buffer struct { array []float64 head int tail int @@ -51,23 +57,23 @@ type ValueBuffer struct { // Len returns the length of the Buffer (as it is currently populated). // Actual memory footprint may be different. -func (b *ValueBuffer) Len() int { +func (b *Buffer) Len() int { return b.size } // GetValue implements seq provider. -func (b *ValueBuffer) GetValue(index int) float64 { +func (b *Buffer) GetValue(index int) float64 { effectiveIndex := (b.head + index) % len(b.array) return b.array[effectiveIndex] } // Capacity returns the total size of the Buffer, including empty elements. -func (b *ValueBuffer) Capacity() int { +func (b *Buffer) Capacity() int { return len(b.array) } // SetCapacity sets the capacity of the Buffer. -func (b *ValueBuffer) SetCapacity(capacity int) { +func (b *Buffer) SetCapacity(capacity int) { newArray := make([]float64, capacity) if b.size > 0 { if b.head < b.tail { @@ -87,7 +93,7 @@ func (b *ValueBuffer) SetCapacity(capacity int) { } // Clear removes all objects from the Buffer. -func (b *ValueBuffer) Clear() { +func (b *Buffer) Clear() { b.array = make([]float64, bufferDefaultCapacity) b.head = 0 b.tail = 0 @@ -95,7 +101,7 @@ func (b *ValueBuffer) Clear() { } // Enqueue adds an element to the "back" of the Buffer. -func (b *ValueBuffer) Enqueue(value float64) { +func (b *Buffer) Enqueue(value float64) { if b.size == len(b.array) { newCapacity := int(len(b.array) * int(bufferGrowFactor/100)) if newCapacity < (len(b.array) + bufferMinimumGrow) { @@ -110,7 +116,7 @@ func (b *ValueBuffer) Enqueue(value float64) { } // Dequeue removes the first element from the RingBuffer. -func (b *ValueBuffer) Dequeue() float64 { +func (b *Buffer) Dequeue() float64 { if b.size == 0 { return 0 } @@ -122,7 +128,7 @@ func (b *ValueBuffer) Dequeue() float64 { } // Peek returns but does not remove the first element. -func (b *ValueBuffer) Peek() float64 { +func (b *Buffer) Peek() float64 { if b.size == 0 { return 0 } @@ -130,7 +136,7 @@ func (b *ValueBuffer) Peek() float64 { } // PeekBack returns but does not remove the last element. -func (b *ValueBuffer) PeekBack() float64 { +func (b *Buffer) PeekBack() float64 { if b.size == 0 { return 0 } @@ -141,7 +147,7 @@ func (b *ValueBuffer) PeekBack() float64 { } // TrimExcess resizes the capacity of the buffer to better fit the contents. -func (b *ValueBuffer) TrimExcess() { +func (b *Buffer) TrimExcess() { threshold := float64(len(b.array)) * 0.9 if b.size < int(threshold) { b.SetCapacity(b.size) @@ -149,7 +155,7 @@ func (b *ValueBuffer) TrimExcess() { } // Array returns the ring buffer, in order, as an array. -func (b *ValueBuffer) Array() Array { +func (b *Buffer) Array() Array { newArray := make([]float64, b.size) if b.size == 0 { @@ -167,7 +173,7 @@ func (b *ValueBuffer) Array() Array { } // Each calls the consumer for each element in the buffer. -func (b *ValueBuffer) Each(mapfn func(int, float64)) { +func (b *Buffer) Each(mapfn func(int, float64)) { if b.size == 0 { return } @@ -191,7 +197,7 @@ func (b *ValueBuffer) Each(mapfn func(int, float64)) { } // String returns a string representation for value buffers. -func (b *ValueBuffer) String() string { +func (b *Buffer) String() string { var values []string for _, elem := range b.Array() { values = append(values, fmt.Sprintf("%v", elem)) diff --git a/seq/buffer_test.go b/seq/buffer_test.go new file mode 100644 index 0000000..19fd64d --- /dev/null +++ b/seq/buffer_test.go @@ -0,0 +1,192 @@ +package seq + +import ( + "testing" + + "github.com/blendlabs/go-assert" +) + +func TestBuffer(t *testing.T) { + assert := assert.New(t) + + buffer := NewBuffer() + + buffer.Enqueue(1) + assert.Equal(1, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(1, buffer.PeekBack()) + + buffer.Enqueue(2) + assert.Equal(2, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(2, buffer.PeekBack()) + + buffer.Enqueue(3) + assert.Equal(3, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(3, buffer.PeekBack()) + + buffer.Enqueue(4) + assert.Equal(4, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(4, buffer.PeekBack()) + + buffer.Enqueue(5) + assert.Equal(5, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(5, buffer.PeekBack()) + + buffer.Enqueue(6) + assert.Equal(6, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(6, buffer.PeekBack()) + + buffer.Enqueue(7) + assert.Equal(7, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(7, buffer.PeekBack()) + + buffer.Enqueue(8) + assert.Equal(8, buffer.Len()) + assert.Equal(1, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value := buffer.Dequeue() + assert.Equal(1, value) + assert.Equal(7, buffer.Len()) + assert.Equal(2, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(2, value) + assert.Equal(6, buffer.Len()) + assert.Equal(3, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(3, value) + assert.Equal(5, buffer.Len()) + assert.Equal(4, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(4, value) + assert.Equal(4, buffer.Len()) + assert.Equal(5, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(5, value) + assert.Equal(3, buffer.Len()) + assert.Equal(6, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(6, value) + assert.Equal(2, buffer.Len()) + assert.Equal(7, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(7, value) + assert.Equal(1, buffer.Len()) + assert.Equal(8, buffer.Peek()) + assert.Equal(8, buffer.PeekBack()) + + value = buffer.Dequeue() + assert.Equal(8, value) + assert.Equal(0, buffer.Len()) + assert.Zero(buffer.Peek()) + assert.Zero(buffer.PeekBack()) +} + +func TestBufferClear(t *testing.T) { + assert := assert.New(t) + + buffer := NewBuffer() + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + buffer.Enqueue(1) + + assert.Equal(8, buffer.Len()) + + buffer.Clear() + assert.Equal(0, buffer.Len()) + assert.Zero(buffer.Peek()) + assert.Zero(buffer.PeekBack()) +} + +func TestBufferArray(t *testing.T) { + assert := assert.New(t) + + buffer := NewBuffer() + buffer.Enqueue(1) + buffer.Enqueue(2) + buffer.Enqueue(3) + buffer.Enqueue(4) + buffer.Enqueue(5) + + contents := buffer.Array() + assert.Len(contents, 5) + assert.Equal(1, contents[0]) + assert.Equal(2, contents[1]) + assert.Equal(3, contents[2]) + assert.Equal(4, contents[3]) + assert.Equal(5, contents[4]) +} + +func TestBufferEach(t *testing.T) { + assert := assert.New(t) + + buffer := NewBuffer() + + for x := 1; x < 17; x++ { + buffer.Enqueue(float64(x)) + } + + called := 0 + buffer.Each(func(_ int, v float64) { + if v == float64(called+1) { + called++ + } + }) + + assert.Equal(16, called) +} + +func TestNewBuffer(t *testing.T) { + assert := assert.New(t) + + empty := NewBuffer() + assert.NotNil(empty) + assert.Zero(empty.Len()) + assert.Equal(bufferDefaultCapacity, empty.Capacity()) + assert.Zero(empty.Peek()) + assert.Zero(empty.PeekBack()) +} + +func TestNewBufferWithValues(t *testing.T) { + assert := assert.New(t) + + values := NewBuffer(1, 2, 3, 4, 5) + assert.NotNil(values) + assert.Equal(5, values.Len()) + assert.Equal(1, values.Peek()) + assert.Equal(5, values.PeekBack()) +} + +func TestBufferGrowth(t *testing.T) { + assert := assert.New(t) + + values := NewBuffer(1, 2, 3, 4, 5) + for i := 0; i < 1<<10; i++ { + values.Enqueue(float64(i)) + } + + assert.Equal(1<<10-1, values.PeekBack()) +} diff --git a/seq/linear.go b/seq/linear.go new file mode 100644 index 0000000..699a5ac --- /dev/null +++ b/seq/linear.go @@ -0,0 +1,73 @@ +package seq + +// Range returns the array values of a linear seq with a given start, end and optional step. +func Range(start, end float64) []float64 { + return Seq{NewLinear().WithStart(start).WithEnd(end).WithStep(1.0)}.Array() +} + +// RangeWithStep returns the array values of a linear seq with a given start, end and optional step. +func RangeWithStep(start, end, step float64) []float64 { + return Seq{NewLinear().WithStart(start).WithEnd(end).WithStep(step)}.Array() +} + +// NewLinear returns a new linear generator. +func NewLinear() *Linear { + return &Linear{step: 1.0} +} + +// Linear is a stepwise generator. +type Linear struct { + start float64 + end float64 + step float64 +} + +// Start returns the start value. +func (lg Linear) Start() float64 { + return lg.start +} + +// End returns the end value. +func (lg Linear) End() float64 { + return lg.end +} + +// Step returns the step value. +func (lg Linear) Step() float64 { + return lg.step +} + +// Len returns the number of elements in the seq. +func (lg Linear) Len() int { + if lg.start < lg.end { + return int((lg.end-lg.start)/lg.step) + 1 + } + return int((lg.start-lg.end)/lg.step) + 1 +} + +// GetValue returns the value at a given index. +func (lg Linear) GetValue(index int) float64 { + fi := float64(index) + if lg.start < lg.end { + return lg.start + (fi * lg.step) + } + return lg.start - (fi * lg.step) +} + +// WithStart sets the start and returns the linear generator. +func (lg *Linear) WithStart(start float64) *Linear { + lg.start = start + return lg +} + +// WithEnd sets the end and returns the linear generator. +func (lg *Linear) WithEnd(end float64) *Linear { + lg.end = end + return lg +} + +// WithStep sets the step and returns the linear generator. +func (lg *Linear) WithStep(step float64) *Linear { + lg.step = step + return lg +} diff --git a/seq/linear_test.go b/seq/linear_test.go new file mode 100644 index 0000000..70c5719 --- /dev/null +++ b/seq/linear_test.go @@ -0,0 +1,48 @@ +package seq + +import ( + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestRange(t *testing.T) { + assert := assert.New(t) + + values := Range(1, 100) + assert.Len(values, 100) + assert.Equal(1, values[0]) + assert.Equal(100, values[99]) +} + +func TestRangeWithStep(t *testing.T) { + assert := assert.New(t) + + values := RangeWithStep(0, 100, 5) + assert.Equal(100, values[20]) + assert.Len(values, 21) +} + +func TestRangeReversed(t *testing.T) { + assert := assert.New(t) + + values := Range(10.0, 1.0) + assert.Equal(10, len(values)) + assert.Equal(10.0, values[0]) + assert.Equal(1.0, values[9]) +} + +func TestValuesRegression(t *testing.T) { + assert := assert.New(t) + + // note; this assumes a 1.0 step is implicitly set in the constructor. + linearProvider := NewLinear().WithStart(1.0).WithEnd(100.0) + assert.Equal(1, linearProvider.Start()) + assert.Equal(100, linearProvider.End()) + assert.Equal(100, linearProvider.Len()) + + values := Seq{Provider: linearProvider}.Array() + assert.Len(values, 100) + assert.Equal(1.0, values[0]) + assert.Equal(100, values[99]) +} diff --git a/seq/provider.go b/seq/provider.go new file mode 100644 index 0000000..ce96d1f --- /dev/null +++ b/seq/provider.go @@ -0,0 +1,15 @@ +package seq + +import "time" + +// Provider is a provider for values for a seq. +type Provider interface { + Len() int + GetValue(int) float64 +} + +// TimeProvider is a provider for values for a seq. +type TimeProvider interface { + Len() int + GetValue(int) time.Time +} diff --git a/random_sequence.go b/seq/random.go similarity index 64% rename from random_sequence.go rename to seq/random.go index 45c9971..ea65084 100644 --- a/random_sequence.go +++ b/seq/random.go @@ -1,4 +1,4 @@ -package chart +package seq import ( "math" @@ -6,29 +6,25 @@ import ( "time" ) -var ( - _ Sequence = (*RandomSeq)(nil) -) - // RandomValues returns an array of random values. func RandomValues(count int) []float64 { - return Seq{NewRandomSequence().WithLen(count)}.Values() + return Seq{NewRandom().WithLen(count)}.Array() } // RandomValuesWithMax returns an array of random values with a given average. func RandomValuesWithMax(count int, max float64) []float64 { - return Seq{NewRandomSequence().WithMax(max).WithLen(count)}.Values() + return Seq{NewRandom().WithMax(max).WithLen(count)}.Array() } -// NewRandomSequence creates a new random seq. -func NewRandomSequence() *RandomSeq { - return &RandomSeq{ +// NewRandom creates a new random seq. +func NewRandom() *Random { + return &Random{ rnd: rand.New(rand.NewSource(time.Now().Unix())), } } -// RandomSeq is a random number seq generator. -type RandomSeq struct { +// Random is a random number seq generator. +type Random struct { rnd *rand.Rand max *float64 min *float64 @@ -36,7 +32,7 @@ type RandomSeq struct { } // Len returns the number of elements that will be generated. -func (r *RandomSeq) Len() int { +func (r *Random) Len() int { if r.len != nil { return *r.len } @@ -44,7 +40,7 @@ func (r *RandomSeq) Len() int { } // GetValue returns the value. -func (r *RandomSeq) GetValue(_ int) float64 { +func (r *Random) GetValue(_ int) float64 { if r.min != nil && r.max != nil { var delta float64 @@ -64,29 +60,29 @@ func (r *RandomSeq) GetValue(_ int) float64 { } // WithLen sets a maximum len -func (r *RandomSeq) WithLen(length int) *RandomSeq { +func (r *Random) WithLen(length int) *Random { r.len = &length return r } // Min returns the minimum value. -func (r RandomSeq) Min() *float64 { +func (r Random) Min() *float64 { return r.min } // WithMin sets the scale and returns the Random. -func (r *RandomSeq) WithMin(min float64) *RandomSeq { +func (r *Random) WithMin(min float64) *Random { r.min = &min return r } // Max returns the maximum value. -func (r RandomSeq) Max() *float64 { +func (r Random) Max() *float64 { return r.max } // WithMax sets the average and returns the Random. -func (r *RandomSeq) WithMax(max float64) *RandomSeq { +func (r *Random) WithMax(max float64) *Random { r.max = &max return r } diff --git a/seq/random_test.go b/seq/random_test.go new file mode 100644 index 0000000..2176909 --- /dev/null +++ b/seq/random_test.go @@ -0,0 +1,20 @@ +package seq + +import ( + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestRandomRegression(t *testing.T) { + assert := assert.New(t) + + randomProvider := NewRandom().WithLen(4096).WithMax(256) + assert.Equal(4096, randomProvider.Len()) + assert.Equal(256, *randomProvider.Max()) + + randomSequence := New(randomProvider) + randomValues := randomSequence.Array() + assert.Len(randomValues, 4096) + assert.InDelta(128, randomSequence.Average(), 10.0) +} diff --git a/seq.go b/seq/seq.go similarity index 75% rename from seq.go rename to seq/seq.go index 76ac40d..606a118 100644 --- a/seq.go +++ b/seq/seq.go @@ -1,34 +1,34 @@ -package chart +package seq import ( "math" "sort" ) -// ValueSequence returns a sequence for a given values set. -func ValueSequence(values ...float64) Seq { - return Seq{NewArray(values...)} +// New wraps a provider with a seq. +func New(provider Provider) Seq { + return Seq{Provider: provider} } -// Sequence is a provider for values for a seq. -type Sequence interface { - Len() int - GetValue(int) float64 +// Values returns a new seq composed of a given set of values. +func Values(values ...float64) Seq { + return Seq{Provider: Array(values)} } // Seq is a utility wrapper for seq providers. type Seq struct { - Sequence + Provider } -// Values enumerates the seq into a slice. -func (s Seq) Values() (output []float64) { - if s.Len() == 0 { +// Array enumerates the seq into a slice. +func (s Seq) Array() (output []float64) { + slen := s.Len() + if slen == 0 { return } - output = make([]float64, s.Len()) - for i := 0; i < s.Len(); i++ { + output = make([]float64, slen) + for i := 0; i < slen; i++ { output[i] = s.GetValue(i) } return @@ -137,36 +137,67 @@ func (s Seq) MinMax() (min, max float64) { return } +// First returns the value at index 0. +func (s Seq) First() float64 { + if s.Len() == 0 { + return 0 + } + return s.GetValue(0) +} + +// Last returns the value at index (len)-1. +func (s Seq) Last() float64 { + if s.Len() == 0 { + return 0 + } + return s.GetValue(s.Len() - 1) +} + // Sort returns the seq sorted in ascending order. // This fully enumerates the seq. func (s Seq) Sort() Seq { if s.Len() == 0 { return s } - values := s.Values() - sort.Float64s(values) - return Seq{Array(values)} + values := s.Array() + sort.Slice(values, func(i, j int) bool { + return values[i] < values[j] + }) + return Seq{Provider: Array(values)} } -// Reverse reverses the sequence -func (s Seq) Reverse() Seq { +// SortDescending returns the seq sorted in descending order. +// This fully enumerates the seq. +func (s Seq) SortDescending() Seq { if s.Len() == 0 { return s } + values := s.Array() + sort.Slice(values, func(i, j int) bool { + return values[i] > values[j] + }) + return Seq{Provider: Array(values)} +} - values := s.Values() - valuesLen := len(values) - valuesLen1 := len(values) - 1 - valuesLen2 := valuesLen >> 1 - var i, j float64 - for index := 0; index < valuesLen2; index++ { - i = values[index] - j = values[valuesLen1-index] - values[index] = j - values[valuesLen1-index] = i +// Reverse reverses the sequence's order. +func (s Seq) Reverse() Seq { + slen := s.Len() + if slen == 0 { + return s } - return Seq{Array(values)} + slen2 := slen >> 1 + values := s.Array() + + i := 0 + j := slen - 1 + for i < slen2 { + values[i], values[j] = values[j], values[i] + i++ + j-- + } + + return Seq{Provider: Array(values)} } // Median returns the median or middle value in the sorted seq. @@ -271,5 +302,5 @@ func (s Seq) Normalize() Seq { output[i] = (s.GetValue(i) - min) / delta } - return Seq{Array(output)} + return Seq{Provider: Array(output)} } diff --git a/seq/seq_test.go b/seq/seq_test.go new file mode 100644 index 0000000..33f8458 --- /dev/null +++ b/seq/seq_test.go @@ -0,0 +1,95 @@ +package seq + +import ( + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestSequenceEach(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + values.Each(func(i int, v float64) { + assert.Equal(i, v-1) + }) +} + +func TestSequenceMap(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + mapped := values.Map(func(i int, v float64) float64 { + assert.Equal(i, v-1) + return v * 2 + }) + assert.Equal(4, mapped.Len()) +} + +func TestSequenceFoldLeft(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + ten := values.FoldLeft(func(_ int, vp, v float64) float64 { + return vp + v + }) + assert.Equal(10, ten) + + orderTest := Seq{NewArray(10, 3, 2, 1)} + four := orderTest.FoldLeft(func(_ int, vp, v float64) float64 { + return vp - v + }) + assert.Equal(4, four) +} + +func TestSequenceFoldRight(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + ten := values.FoldRight(func(_ int, vp, v float64) float64 { + return vp + v + }) + assert.Equal(10, ten) + + orderTest := Seq{NewArray(10, 3, 2, 1)} + notFour := orderTest.FoldRight(func(_ int, vp, v float64) float64 { + return vp - v + }) + assert.Equal(-14, notFour) +} + +func TestSequenceSum(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + assert.Equal(10, values.Sum()) +} + +func TestSequenceAverage(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + assert.Equal(2.5, values.Average()) + + valuesOdd := Seq{NewArray(1, 2, 3, 4, 5)} + assert.Equal(3, valuesOdd.Average()) +} + +func TestSequenceVariance(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4, 5)} + assert.Equal(2, values.Variance()) +} + +func TestSequenceNormalize(t *testing.T) { + assert := assert.New(t) + + normalized := Values(1, 2, 3, 4, 5).Normalize().Array() + + assert.NotEmpty(normalized) + assert.Len(normalized, 5) + assert.Equal(0, normalized[0]) + assert.Equal(0.25, normalized[1]) + assert.Equal(1, normalized[4]) +} diff --git a/seq/time.go b/seq/time.go new file mode 100644 index 0000000..65c9bac --- /dev/null +++ b/seq/time.go @@ -0,0 +1,131 @@ +package seq + +import ( + "time" + + "github.com/wcharczuk/go-chart/util" +) + +// TimeUtil is a utility singleton with helper functions for time seq generation. +var TimeUtil timeUtil + +type timeUtil struct{} + +func (tu timeUtil) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { + var times []time.Time + cursor := util.Date.On(marketOpen, from) + toClose := util.Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + todayOpen := util.Date.On(marketOpen, cursor) + todayClose := util.Date.On(marketClose, cursor) + isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) + + if (cursor.Equal(todayOpen) || cursor.After(todayOpen)) && (cursor.Equal(todayClose) || cursor.Before(todayClose)) && isValidTradingDay { + times = append(times, cursor) + } + if cursor.After(todayClose) { + cursor = util.Date.NextMarketOpen(cursor, marketOpen, isHoliday) + } else { + cursor = util.Date.NextHour(cursor) + } + } + return times +} + +func (tu timeUtil) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { + var times []time.Time + cursor := util.Date.On(marketOpen, from) + toClose := util.Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + + isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) + + if isValidTradingDay { + todayOpen := util.Date.On(marketOpen, cursor) + todayNoon := util.Date.NoonOn(cursor) + today2pm := util.Date.On(util.Date.Time(14, 0, 0, 0, cursor.Location()), cursor) + todayClose := util.Date.On(marketClose, cursor) + times = append(times, todayOpen, todayNoon, today2pm, todayClose) + } + + cursor = util.Date.NextDay(cursor) + } + return times +} + +func (tu timeUtil) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { + var times []time.Time + cursor := util.Date.On(marketOpen, from) + toClose := util.Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + newValue := util.Date.NoonOn(cursor) + times = append(times, newValue) + } + + cursor = util.Date.NextDay(cursor) + } + return times +} + +func (tu timeUtil) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { + var times []time.Time + cursor := util.Date.On(marketOpen, from) + toClose := util.Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + todayClose := util.Date.On(marketClose, cursor) + times = append(times, todayClose) + } + + cursor = cursor.AddDate(0, 0, 2) + } + return times +} + +func (tu timeUtil) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { + var times []time.Time + cursor := util.Date.On(marketClose, from) + toClose := util.Date.On(marketClose, to) + + for cursor.Equal(toClose) || cursor.Before(toClose) { + isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + times = append(times, cursor) + } + cursor = util.Date.NextDayOfWeek(cursor, time.Monday) + } + return times +} + +func (tu timeUtil) Hours(start time.Time, totalHours int) []time.Time { + times := make([]time.Time, totalHours) + + last := start + for i := 0; i < totalHours; i++ { + times[i] = last + last = last.Add(time.Hour) + } + + return times +} + +// HoursFilled adds zero values for the data bounded by the start and end of the xdata array. +func (tu timeUtil) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { + start, end := Times(xdata...).MinAndMax() + + totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end)) + + finalTimes := tu.Hours(start, totalHours+1) + finalValues := make([]float64, totalHours+1) + + var hoursFromStart int + for i, xd := range xdata { + hoursFromStart = util.Date.DiffHours(start, xd) + finalValues[hoursFromStart] = ydata[i] + } + + return finalTimes, finalValues +} diff --git a/seq/time_seq.go b/seq/time_seq.go new file mode 100644 index 0000000..a7f0613 --- /dev/null +++ b/seq/time_seq.go @@ -0,0 +1,261 @@ +package seq + +import ( + "sort" + "time" +) + +var ( + // TimeZero is the zero time. + TimeZero = time.Time{} +) + +// Times returns a new time sequence. +func Times(values ...time.Time) TimeSeq { + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// TimeSeq is a sequence of times. +type TimeSeq struct { + TimeProvider +} + +// Array converts the sequence to times. +func (ts TimeSeq) Array() (output []time.Time) { + slen := ts.Len() + if slen == 0 { + return + } + + output = make([]time.Time, slen) + for i := 0; i < slen; i++ { + output[i] = ts.GetValue(i) + } + return +} + +// Each applies the `mapfn` to all values in the value provider. +func (ts TimeSeq) Each(mapfn func(int, time.Time)) { + for i := 0; i < ts.Len(); i++ { + mapfn(i, ts.GetValue(i)) + } +} + +// Map applies the `mapfn` to all values in the value provider, +// returning a new seq. +func (ts TimeSeq) Map(mapfn func(int, time.Time) time.Time) TimeSeq { + output := make([]time.Time, ts.Len()) + for i := 0; i < ts.Len(); i++ { + mapfn(i, ts.GetValue(i)) + } + return TimeSeq{ArrayOfTimes(output)} +} + +// FoldLeft collapses a seq from left to right. +func (ts TimeSeq) FoldLeft(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) { + tslen := ts.Len() + if tslen == 0 { + return TimeZero + } + + if tslen == 1 { + return ts.GetValue(0) + } + + v0 = ts.GetValue(0) + for i := 1; i < tslen; i++ { + v0 = mapfn(i, v0, ts.GetValue(i)) + } + return +} + +// FoldRight collapses a seq from right to left. +func (ts TimeSeq) FoldRight(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) { + tslen := ts.Len() + if tslen == 0 { + return TimeZero + } + + if tslen == 1 { + return ts.GetValue(0) + } + + v0 = ts.GetValue(tslen - 1) + for i := tslen - 2; i >= 0; i-- { + v0 = mapfn(i, v0, ts.GetValue(i)) + } + return +} + +// Sort returns the seq in ascending order. +func (ts TimeSeq) Sort() TimeSeq { + if ts.Len() == 0 { + return ts + } + + values := ts.Array() + sort.Slice(values, func(i, j int) bool { + return values[i].Before(values[j]) + }) + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// SortDescending returns the seq in descending order. +func (ts TimeSeq) SortDescending() TimeSeq { + if ts.Len() == 0 { + return ts + } + + values := ts.Array() + sort.Slice(values, func(i, j int) bool { + return values[i].After(values[j]) + }) + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// Min returns the minimum (or earliest) time in the sequence. +func (ts TimeSeq) Min() (min time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + min = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.Before(min) { + min = tv + } + } + return +} + +// Start is an alias to `Min`. +func (ts TimeSeq) Start() time.Time { + return ts.Min() +} + +// Max returns the maximum (or latest) time in the sequence. +func (ts TimeSeq) Max() (max time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + max = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.After(max) { + max = tv + } + } + return +} + +// End is an alias to `Max`. +func (ts TimeSeq) End() time.Time { + return ts.Max() +} + +// First returns the first value in the sequence. +func (ts TimeSeq) First() time.Time { + if ts.Len() == 0 { + return TimeZero + } + + return ts.GetValue(0) +} + +// Last returns the last value in the sequence. +func (ts TimeSeq) Last() time.Time { + if ts.Len() == 0 { + return TimeZero + } + + return ts.GetValue(ts.Len() - 1) +} + +// MinAndMax returns both the earliest and latest value from a sequence in one pass. +func (ts TimeSeq) MinAndMax() (min, max time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + min = ts.GetValue(0) + max = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.Before(min) { + min = tv + } + if tv.After(max) { + max = tv + } + } + return +} + +// MapDistinct maps values given a map function to their distinct outputs. +func (ts TimeSeq) MapDistinct(mapFn func(time.Time) time.Time) TimeSeq { + tslen := ts.Len() + if tslen == 0 { + return TimeSeq{} + } + + var output []time.Time + hourLookup := SetOfTime{} + + // add the initial value + tv := ts.GetValue(0) + tvh := mapFn(tv) + hourLookup.Add(tvh) + output = append(output, tvh) + + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + tvh = mapFn(tv) + if !hourLookup.Has(tvh) { + hourLookup.Add(tvh) + output = append(output, tvh) + } + } + + return TimeSeq{ArrayOfTimes(output)} +} + +// Hours returns times in each distinct hour represented by the sequence. +func (ts TimeSeq) Hours() TimeSeq { + return ts.MapDistinct(ts.trimToHour) +} + +// Days returns times in each distinct day represented by the sequence. +func (ts TimeSeq) Days() TimeSeq { + return ts.MapDistinct(ts.trimToDay) +} + +// Months returns times in each distinct months represented by the sequence. +func (ts TimeSeq) Months() TimeSeq { + return ts.MapDistinct(ts.trimToMonth) +} + +// Years returns times in each distinc year represented by the sequence. +func (ts TimeSeq) Years() TimeSeq { + return ts.MapDistinct(ts.trimToYear) +} + +func (ts TimeSeq) trimToHour(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), tv.Day(), tv.Hour(), 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToDay(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), tv.Day(), 0, 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToMonth(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), 1, 0, 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToYear(tv time.Time) time.Time { + return time.Date(tv.Year(), 1, 1, 0, 0, 0, 0, tv.Location()) +} diff --git a/seq/time_seq_test.go b/seq/time_seq_test.go new file mode 100644 index 0000000..b9cf0b9 --- /dev/null +++ b/seq/time_seq_test.go @@ -0,0 +1,81 @@ +package seq + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" +) + +func TestTimeSeqTimes(t *testing.T) { + assert := assert.New(t) + + seq := Times(time.Now(), time.Now(), time.Now()) + assert.Equal(3, seq.Len()) +} + +func parseTime(str string) time.Time { + tv, _ := time.Parse("2006-01-02 15:04:05", str) + return tv +} + +func TestTimeSeqSort(t *testing.T) { + assert := assert.New(t) + + seq := Times( + parseTime("2016-05-14 12:00:00"), + parseTime("2017-05-14 12:00:00"), + parseTime("2015-05-14 12:00:00"), + parseTime("2017-05-13 12:00:00"), + ) + + sorted := seq.Sort() + assert.Equal(4, sorted.Len()) + min, max := sorted.MinAndMax() + assert.Equal(parseTime("2015-05-14 12:00:00"), min) + assert.Equal(parseTime("2017-05-14 12:00:00"), max) + + first, last := sorted.First(), sorted.Last() + assert.Equal(min, first) + assert.Equal(max, last) +} + +func TestTimeSeqSortDescending(t *testing.T) { + assert := assert.New(t) + + seq := Times( + parseTime("2016-05-14 12:00:00"), + parseTime("2017-05-14 12:00:00"), + parseTime("2015-05-14 12:00:00"), + parseTime("2017-05-13 12:00:00"), + ) + + sorted := seq.SortDescending() + assert.Equal(4, sorted.Len()) + min, max := sorted.MinAndMax() + assert.Equal(parseTime("2015-05-14 12:00:00"), min) + assert.Equal(parseTime("2017-05-14 12:00:00"), max) + + first, last := sorted.First(), sorted.Last() + assert.Equal(max, first) + assert.Equal(min, last) +} + +func TestTimeSeqDays(t *testing.T) { + assert := assert.New(t) + + seq := Times( + parseTime("2017-05-10 12:00:00"), + parseTime("2017-05-10 16:00:00"), + parseTime("2017-05-11 12:00:00"), + parseTime("2015-05-12 12:00:00"), + parseTime("2015-05-12 16:00:00"), + parseTime("2017-05-13 12:00:00"), + parseTime("2017-05-14 12:00:00"), + ) + + days := seq.Days() + assert.Equal(5, days.Len()) + assert.Equal(10, days.First().Day()) + assert.Equal(14, days.Last().Day()) +} diff --git a/seq/time_test.go b/seq/time_test.go new file mode 100644 index 0000000..40bd83f --- /dev/null +++ b/seq/time_test.go @@ -0,0 +1,111 @@ +package seq + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" +) + +func TestTimeMarketHours(t *testing.T) { + assert := assert.New(t) + + today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) + mh := TimeUtil.MarketHours(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) + assert.Len(mh, 8) + assert.Equal(util.Date.Eastern(), mh[0].Location()) +} + +func TestTimeMarketHourQuarters(t *testing.T) { + assert := assert.New(t) + today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) + mh := TimeUtil.MarketHourQuarters(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) + assert.Len(mh, 4) + assert.Equal(9, mh[0].Hour()) + assert.Equal(30, mh[0].Minute()) + assert.Equal(util.Date.Eastern(), mh[0].Location()) + + assert.Equal(12, mh[1].Hour()) + assert.Equal(00, mh[1].Minute()) + assert.Equal(util.Date.Eastern(), mh[1].Location()) + + assert.Equal(14, mh[2].Hour()) + assert.Equal(00, mh[2].Minute()) + assert.Equal(util.Date.Eastern(), mh[2].Location()) +} + +func TestTimeHours(t *testing.T) { + assert := assert.New(t) + + today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC) + seq := TimeUtil.Hours(today, 24) + + end := Times(seq...).Max() + assert.Len(seq, 24) + assert.Equal(2016, end.Year()) + assert.Equal(07, int(end.Month())) + assert.Equal(02, end.Day()) + assert.Equal(11, end.Hour()) +} + +func TestSequenceHoursFill(t *testing.T) { + assert := assert.New(t) + + xdata := []time.Time{ + time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC), + time.Date(2016, 07, 01, 13, 0, 0, 0, time.UTC), + time.Date(2016, 07, 01, 14, 0, 0, 0, time.UTC), + time.Date(2016, 07, 02, 4, 0, 0, 0, time.UTC), + time.Date(2016, 07, 02, 5, 0, 0, 0, time.UTC), + time.Date(2016, 07, 03, 12, 0, 0, 0, time.UTC), + time.Date(2016, 07, 03, 14, 0, 0, 0, time.UTC), + } + + ydata := []float64{ + 1.1, + 1.2, + 1.4, + 0.8, + 2.1, + 0.4, + 0.6, + } + + filledTimes, filledValues := TimeUtil.HoursFilled(xdata, ydata) + assert.Len(filledTimes, util.Date.DiffHours(Times(xdata...).Start(), Times(xdata...).End())+1) + assert.Equal(len(filledValues), len(filledTimes)) + + assert.NotZero(filledValues[0]) + assert.NotZero(filledValues[len(filledValues)-1]) + + assert.NotZero(filledValues[16]) +} + +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(Times(times...).Start(), 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(Times(times...).End(), times[2], time.Millisecond) +} diff --git a/seq/util.go b/seq/util.go new file mode 100644 index 0000000..238537b --- /dev/null +++ b/seq/util.go @@ -0,0 +1,56 @@ +package seq + +import ( + "math" + "time" + + "github.com/wcharczuk/go-chart/util" +) + +func round(input float64, places int) (rounded float64) { + if math.IsNaN(input) { + return 0.0 + } + + sign := 1.0 + if input < 0 { + sign = -1 + input *= -1 + } + + precision := math.Pow(10, float64(places)) + digit := input * precision + _, decimal := math.Modf(digit) + + if decimal >= 0.5 { + rounded = math.Ceil(digit) + } else { + rounded = math.Floor(digit) + } + + return rounded / precision * sign +} + +func f64i(value float64) int { + r := round(value, 0) + return int(r) +} + +// SetOfTime is a simple hash set for timestamps as float64s. +type SetOfTime map[float64]bool + +// Add adds the value to the hash set. +func (sot SetOfTime) Add(tv time.Time) { + sot[util.Time.ToFloat64(tv)] = true +} + +// Has returns if the set contains a given time. +func (sot SetOfTime) Has(tv time.Time) bool { + _, hasValue := sot[util.Time.ToFloat64(tv)] + return hasValue +} + +// Remove removes the value from the set. +func (sot SetOfTime) Remove(tv time.Time) { + delete(sot, util.Time.ToFloat64(tv)) +} diff --git a/seq_test.go b/seq_test.go deleted file mode 100644 index 585900b..0000000 --- a/seq_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestSeqEach(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - values.Each(func(i int, v float64) { - testutil.AssertEqual(t, i, v-1) - }) -} - -func TestSeqMap(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - mapped := values.Map(func(i int, v float64) float64 { - testutil.AssertEqual(t, i, v-1) - return v * 2 - }) - testutil.AssertEqual(t, 4, mapped.Len()) -} - -func TestSeqFoldLeft(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - ten := values.FoldLeft(func(_ int, vp, v float64) float64 { - return vp + v - }) - testutil.AssertEqual(t, 10, ten) - - orderTest := Seq{NewArray(10, 3, 2, 1)} - four := orderTest.FoldLeft(func(_ int, vp, v float64) float64 { - return vp - v - }) - testutil.AssertEqual(t, 4, four) -} - -func TestSeqFoldRight(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - ten := values.FoldRight(func(_ int, vp, v float64) float64 { - return vp + v - }) - testutil.AssertEqual(t, 10, ten) - - orderTest := Seq{NewArray(10, 3, 2, 1)} - notFour := orderTest.FoldRight(func(_ int, vp, v float64) float64 { - return vp - v - }) - testutil.AssertEqual(t, -14, notFour) -} - -func TestSeqSum(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - testutil.AssertEqual(t, 10, values.Sum()) -} - -func TestSeqAverage(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4)} - testutil.AssertEqual(t, 2.5, values.Average()) - - valuesOdd := Seq{NewArray(1, 2, 3, 4, 5)} - testutil.AssertEqual(t, 3, valuesOdd.Average()) -} - -func TestSequenceVariance(t *testing.T) { - // replaced new assertions helper - - values := Seq{NewArray(1, 2, 3, 4, 5)} - testutil.AssertEqual(t, 2, values.Variance()) -} - -func TestSequenceNormalize(t *testing.T) { - // replaced new assertions helper - - normalized := ValueSequence(1, 2, 3, 4, 5).Normalize().Values() - - testutil.AssertNotEmpty(t, normalized) - testutil.AssertLen(t, normalized, 5) - testutil.AssertEqual(t, 0, normalized[0]) - testutil.AssertEqual(t, 0.25, normalized[1]) - testutil.AssertEqual(t, 1, normalized[4]) -} - -func TestLinearRange(t *testing.T) { - // replaced new assertions helper - - values := LinearRange(1, 100) - testutil.AssertLen(t, values, 100) - testutil.AssertEqual(t, 1, values[0]) - testutil.AssertEqual(t, 100, values[99]) -} - -func TestLinearRangeWithStep(t *testing.T) { - // replaced new assertions helper - - values := LinearRangeWithStep(0, 100, 5) - testutil.AssertEqual(t, 100, values[20]) - testutil.AssertLen(t, values, 21) -} - -func TestLinearRangeReversed(t *testing.T) { - // replaced new assertions helper - - values := LinearRange(10.0, 1.0) - testutil.AssertEqual(t, 10, len(values)) - testutil.AssertEqual(t, 10.0, values[0]) - testutil.AssertEqual(t, 1.0, values[9]) -} - -func TestLinearSequenceRegression(t *testing.T) { - // replaced new assertions helper - - // note; this assumes a 1.0 step is implicitly set in the constructor. - linearProvider := NewLinearSequence().WithStart(1.0).WithEnd(100.0) - testutil.AssertEqual(t, 1, linearProvider.Start()) - testutil.AssertEqual(t, 100, linearProvider.End()) - testutil.AssertEqual(t, 100, linearProvider.Len()) - - values := Seq{linearProvider}.Values() - testutil.AssertLen(t, values, 100) - testutil.AssertEqual(t, 1.0, values[0]) - testutil.AssertEqual(t, 100, values[99]) -} diff --git a/sma_series.go b/sma_series.go index b952c0a..396ecaa 100644 --- a/sma_series.go +++ b/sma_series.go @@ -2,6 +2,8 @@ package chart import ( "fmt" + + util "github.com/wcharczuk/go-chart/util" ) const ( @@ -9,13 +11,6 @@ const ( DefaultSimpleMovingAveragePeriod = 16 ) -// Interface Assertions. -var ( - _ Series = (*SMASeries)(nil) - _ FirstValuesProvider = (*SMASeries)(nil) - _ LastValuesProvider = (*SMASeries)(nil) -) - // SMASeries is a computed series. type SMASeries struct { Name string @@ -68,17 +63,6 @@ func (sma SMASeries) GetValues(index int) (x, y float64) { return } -// GetFirstValues computes the first moving average value. -func (sma SMASeries) GetFirstValues() (x, y float64) { - if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { - return - } - px, _ := sma.InnerSeries.GetValues(0) - x = px - y = sma.getAverage(0) - return -} - // GetLastValues computes the last moving average value but walking back window size samples, // and recomputing the last moving average chunk. func (sma SMASeries) GetLastValues() (x, y float64) { @@ -94,7 +78,7 @@ func (sma SMASeries) GetLastValues() (x, y float64) { func (sma SMASeries) getAverage(index int) float64 { period := sma.GetPeriod() - floor := MaxInt(0, index-period) + floor := util.Math.MaxInt(0, index-period) var accum float64 var count float64 for x := index; x >= floor; x-- { diff --git a/sma_series_test.go b/sma_series_test.go index 3951b02..fabb06a 100644 --- a/sma_series_test.go +++ b/sma_series_test.go @@ -3,7 +3,9 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/seq" + "github.com/wcharczuk/go-chart/util" ) type mockValuesProvider struct { @@ -12,14 +14,14 @@ type mockValuesProvider struct { } func (m mockValuesProvider) Len() int { - return MinInt(len(m.X), len(m.Y)) + return util.Math.MinInt(len(m.X), len(m.Y)) } func (m mockValuesProvider) GetValues(index int) (x, y float64) { if index < 0 { panic("negative index at GetValue()") } - if index >= MinInt(len(m.X), len(m.Y)) { + if index >= util.Math.MinInt(len(m.X), len(m.Y)) { panic("index is outside the length of m.X or m.Y") } x = m.X[index] @@ -28,13 +30,13 @@ func (m mockValuesProvider) GetValues(index int) (x, y float64) { } func TestSMASeriesGetValue(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mockSeries := mockValuesProvider{ - LinearRange(1.0, 10.0), - LinearRange(10, 1.0), + seq.Range(1.0, 10.0), + seq.Range(10, 1.0), } - testutil.AssertEqual(t, 10, mockSeries.Len()) + assert.Equal(10, mockSeries.Len()) mas := &SMASeries{ InnerSeries: mockSeries, @@ -47,25 +49,25 @@ func TestSMASeriesGetValue(t *testing.T) { yvalues = append(yvalues, y) } - testutil.AssertEqual(t, 10.0, yvalues[0]) - testutil.AssertEqual(t, 9.5, yvalues[1]) - testutil.AssertEqual(t, 9.0, yvalues[2]) - testutil.AssertEqual(t, 8.5, yvalues[3]) - testutil.AssertEqual(t, 8.0, yvalues[4]) - testutil.AssertEqual(t, 7.5, yvalues[5]) - testutil.AssertEqual(t, 7.0, yvalues[6]) - testutil.AssertEqual(t, 6.5, yvalues[7]) - testutil.AssertEqual(t, 6.0, yvalues[8]) + assert.Equal(10.0, yvalues[0]) + assert.Equal(9.5, yvalues[1]) + assert.Equal(9.0, yvalues[2]) + assert.Equal(8.5, yvalues[3]) + assert.Equal(8.0, yvalues[4]) + assert.Equal(7.5, yvalues[5]) + assert.Equal(7.0, yvalues[6]) + assert.Equal(6.5, yvalues[7]) + assert.Equal(6.0, yvalues[8]) } func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mockSeries := mockValuesProvider{ - LinearRange(1.0, 10.0), - LinearRange(10, 1.0), + seq.Range(1.0, 10.0), + seq.Range(10, 1.0), } - testutil.AssertEqual(t, 10, mockSeries.Len()) + assert.Equal(10, mockSeries.Len()) mas := &SMASeries{ InnerSeries: mockSeries, @@ -79,19 +81,19 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { } lx, ly := mas.GetLastValues() - testutil.AssertEqual(t, 10.0, lx) - testutil.AssertEqual(t, 5.5, ly) - testutil.AssertEqual(t, yvalues[len(yvalues)-1], ly) + assert.Equal(10.0, lx) + assert.Equal(5.5, ly) + assert.Equal(yvalues[len(yvalues)-1], ly) } func TestSMASeriesGetLastValue(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) mockSeries := mockValuesProvider{ - LinearRange(1.0, 100.0), - LinearRange(100, 1.0), + seq.Range(1.0, 100.0), + seq.Range(100, 1.0), } - testutil.AssertEqual(t, 100, mockSeries.Len()) + assert.Equal(100, mockSeries.Len()) mas := &SMASeries{ InnerSeries: mockSeries, @@ -105,7 +107,7 @@ func TestSMASeriesGetLastValue(t *testing.T) { } lx, ly := mas.GetLastValues() - testutil.AssertEqual(t, 100.0, lx) - testutil.AssertEqual(t, 6, ly) - testutil.AssertEqual(t, yvalues[len(yvalues)-1], ly) + assert.Equal(100.0, lx) + assert.Equal(6, ly) + assert.Equal(yvalues[len(yvalues)-1], ly) } diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go index b0c781c..b7f24f2 100644 --- a/stacked_bar_chart.go +++ b/stacked_bar_chart.go @@ -7,6 +7,8 @@ import ( "math" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/seq" + util "github.com/wcharczuk/go-chart/util" ) // StackedBar is a bar within a StackedBarChart. @@ -29,8 +31,6 @@ type StackedBarChart struct { Title string TitleStyle Style - ColorPalette ColorPalette - Width int Height int DPI float64 @@ -46,8 +46,6 @@ type StackedBarChart struct { Font *truetype.Font defaultFont *truetype.Font - IsHorizontal bool - Bars []StackedBar Elements []Renderable } @@ -115,26 +113,10 @@ func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error { } r.SetDPI(sbc.GetDPI(DefaultDPI)) - var canvasBox Box - if sbc.IsHorizontal { - canvasBox = sbc.getHorizontalAdjustedCanvasBox(r, sbc.getDefaultCanvasBox()) - if err := canvasBox.Validate(); err != nil { - return fmt.Errorf("invalid canvas box: %w", err) - } - sbc.drawCanvas(r, canvasBox) - sbc.drawHorizontalBars(r, canvasBox) - sbc.drawHorizontalXAxis(r, canvasBox) - sbc.drawHorizontalYAxis(r, canvasBox) - } else { - canvasBox = sbc.getAdjustedCanvasBox(r, sbc.getDefaultCanvasBox()) - if err := canvasBox.Validate(); err != nil { - return fmt.Errorf("invalid canvas box: %w", err) - } - sbc.drawCanvas(r, canvasBox) - sbc.drawBars(r, canvasBox) - sbc.drawXAxis(r, canvasBox) - sbc.drawYAxis(r, canvasBox) - } + canvasBox := sbc.getAdjustedCanvasBox(r, sbc.getDefaultCanvasBox()) + sbc.drawBars(r, canvasBox) + sbc.drawXAxis(r, canvasBox) + sbc.drawYAxis(r, canvasBox) sbc.drawTitle(r) for _, a := range sbc.Elements { @@ -144,10 +126,6 @@ func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error { return r.Save(w) } -func (sbc StackedBarChart) drawCanvas(r Renderer, canvasBox Box) { - Draw.Box(r, canvasBox, sbc.getCanvasStyle()) -} - func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) { xoffset := canvasBox.Left for _, bar := range sbc.Bars { @@ -156,14 +134,6 @@ func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) { } } -func (sbc StackedBarChart) drawHorizontalBars(r Renderer, canvasBox Box) { - yOffset := canvasBox.Top - for _, bar := range sbc.Bars { - sbc.drawHorizontalBar(r, canvasBox, yOffset, bar) - yOffset += sbc.GetBarSpacing() + bar.GetWidth() - } -} - func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int { barSpacing2 := sbc.GetBarSpacing() >> 1 bxl := xoffset + barSpacing2 @@ -177,93 +147,17 @@ func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar S Top: yoffset, Left: bxl, Right: bxr, - Bottom: MinInt(yoffset+barHeight, canvasBox.Bottom-DefaultStrokeWidth), + Bottom: util.Math.MinInt(yoffset+barHeight, canvasBox.Bottom-DefaultStrokeWidth), } Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index))) yoffset += barHeight } - // draw the labels - yoffset = canvasBox.Top - var lx, ly int - for index, bv := range normalizedBarComponents { - barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Height()))) - - if len(bv.Label) > 0 { - lx = bxl + ((bxr - bxl) / 2) - ly = yoffset + (barHeight / 2) - - bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)).WriteToRenderer(r) - tb := r.MeasureText(bv.Label) - lx = lx - (tb.Width() >> 1) - ly = ly + (tb.Height() >> 1) - - if lx < 0 { - lx = 0 - } - if ly < 0 { - lx = 0 - } - - r.Text(bv.Label, lx, ly) - } - yoffset += barHeight - } - return bxr } -func (sbc StackedBarChart) drawHorizontalBar(r Renderer, canvasBox Box, yoffset int, bar StackedBar) { - halfBarSpacing := sbc.GetBarSpacing() >> 1 - - boxTop := yoffset + halfBarSpacing - boxBottom := boxTop + bar.GetWidth() - - normalizedBarComponents := Values(bar.Values).Normalize() - - xOffset := canvasBox.Right - for index, bv := range normalizedBarComponents { - barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width()))) - barBox := Box{ - Top: boxTop, - Left: MinInt(xOffset-barHeight, canvasBox.Left+DefaultStrokeWidth), - Right: xOffset, - Bottom: boxBottom, - } - Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index))) - xOffset -= barHeight - } - - // draw the labels - xOffset = canvasBox.Right - var lx, ly int - for index, bv := range normalizedBarComponents { - barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width()))) - - if len(bv.Label) > 0 { - lx = xOffset - (barHeight / 2) - ly = boxTop + ((boxBottom - boxTop) / 2) - - bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)).WriteToRenderer(r) - tb := r.MeasureText(bv.Label) - lx = lx - (tb.Width() >> 1) - ly = ly + (tb.Height() >> 1) - - if lx < 0 { - lx = 0 - } - if ly < 0 { - lx = 0 - } - - r.Text(bv.Label, lx, ly) - } - xOffset -= barHeight - } -} - func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { - if !sbc.XAxis.Hidden { + if sbc.XAxis.Show { axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) @@ -296,44 +190,8 @@ func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { } } -func (sbc StackedBarChart) drawHorizontalXAxis(r Renderer, canvasBox Box) { - if !sbc.XAxis.Hidden { - axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) - axisStyle.WriteToRenderer(r) - r.MoveTo(canvasBox.Left, canvasBox.Bottom) - r.LineTo(canvasBox.Right, canvasBox.Bottom) - r.Stroke() - - r.MoveTo(canvasBox.Left, canvasBox.Bottom) - r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight) - r.Stroke() - - ticks := LinearRangeWithStep(0.0, 1.0, 0.2) - for _, t := range ticks { - axisStyle.GetStrokeOptions().WriteToRenderer(r) - tx := canvasBox.Left + int(t*float64(canvasBox.Width())) - r.MoveTo(tx, canvasBox.Bottom) - r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight) - r.Stroke() - - axisStyle.GetTextOptions().WriteToRenderer(r) - text := fmt.Sprintf("%0.0f%%", t*100) - - textBox := r.MeasureText(text) - textX := tx - (textBox.Width() >> 1) - textY := canvasBox.Bottom + DefaultXAxisMargin + 10 - - if t == 1 { - textX = canvasBox.Right - textBox.Width() - } - - Draw.Text(r, text, textX, textY, axisStyle) - } - } -} - func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { - if !sbc.YAxis.Hidden { + if sbc.YAxis.Show { axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) r.MoveTo(canvasBox.Right, canvasBox.Top) @@ -344,7 +202,7 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom) r.Stroke() - ticks := LinearRangeWithStep(0.0, 1.0, 0.2) + ticks := seq.RangeWithStep(0.0, 1.0, 0.2) for _, t := range ticks { axisStyle.GetStrokeOptions().WriteToRenderer(r) ty := canvasBox.Bottom - int(t*float64(canvasBox.Height())) @@ -356,83 +214,18 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { text := fmt.Sprintf("%0.0f%%", t*100) tb := r.MeasureText(text) - Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle) + Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(int(tb.Height())>>1), axisStyle) } - } -} -func (sbc StackedBarChart) drawHorizontalYAxis(r Renderer, canvasBox Box) { - if !sbc.YAxis.Hidden { - axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes()) - axisStyle.WriteToRenderer(r) - - r.MoveTo(canvasBox.Left, canvasBox.Bottom) - r.LineTo(canvasBox.Left, canvasBox.Top) - r.Stroke() - - r.MoveTo(canvasBox.Left, canvasBox.Bottom) - r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, canvasBox.Bottom) - r.Stroke() - - cursor := canvasBox.Top - for _, bar := range sbc.Bars { - barLabelBox := Box{ - Top: cursor, - Left: 0, - Right: canvasBox.Left - DefaultYAxisMargin, - Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(), - } - if len(bar.Name) > 0 { - Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle) - } - axisStyle.WriteToRenderer(r) - r.MoveTo(canvasBox.Left, barLabelBox.Bottom) - r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, barLabelBox.Bottom) - r.Stroke() - cursor += bar.GetWidth() + sbc.GetBarSpacing() - } } } func (sbc StackedBarChart) drawTitle(r Renderer) { - if len(sbc.Title) > 0 && !sbc.TitleStyle.Hidden { - r.SetFont(sbc.TitleStyle.GetFont(sbc.GetFont())) - r.SetFontColor(sbc.TitleStyle.GetFontColor(sbc.GetColorPalette().TextColor())) - titleFontSize := sbc.TitleStyle.GetFontSize(DefaultTitleFontSize) - r.SetFontSize(titleFontSize) - - textBox := r.MeasureText(sbc.Title) - - textWidth := textBox.Width() - textHeight := textBox.Height() - - titleX := (sbc.GetWidth() >> 1) - (textWidth >> 1) - titleY := sbc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight - - r.Text(sbc.Title, titleX, titleY) + if len(sbc.Title) > 0 && sbc.TitleStyle.Show { + Draw.TextWithin(r, sbc.Title, sbc.Box(), sbc.styleDefaultsTitle()) } } -func (sbc StackedBarChart) getCanvasStyle() Style { - return sbc.Canvas.InheritFrom(sbc.styleDefaultsCanvas()) -} - -func (sbc StackedBarChart) styleDefaultsCanvas() Style { - return Style{ - FillColor: sbc.GetColorPalette().CanvasColor(), - StrokeColor: sbc.GetColorPalette().CanvasStrokeColor(), - StrokeWidth: DefaultCanvasStrokeWidth, - } -} - -// GetColorPalette returns the color palette for the chart. -func (sbc StackedBarChart) GetColorPalette() ColorPalette { - if sbc.ColorPalette != nil { - return sbc.ColorPalette - } - return AlternateColorPalette -} - func (sbc StackedBarChart) getDefaultCanvasBox() Box { return sbc.Box() } @@ -443,7 +236,7 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box { totalWidth += bar.GetWidth() + sbc.GetBarSpacing() } - if !sbc.XAxis.Hidden { + if sbc.XAxis.Show { xaxisHeight := DefaultVerticalTickHeight axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) @@ -461,7 +254,7 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box { lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle) linesBox := Text.MeasureLines(r, lines, axisStyle) - xaxisHeight = MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight) + xaxisHeight = util.Math.MaxInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight) } } return Box{ @@ -480,56 +273,14 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box { } -func (sbc StackedBarChart) getHorizontalAdjustedCanvasBox(r Renderer, canvasBox Box) Box { - var totalHeight int - for _, bar := range sbc.Bars { - totalHeight += bar.GetWidth() + sbc.GetBarSpacing() - } - - if !sbc.YAxis.Hidden { - yAxisWidth := DefaultHorizontalTickWidth - - axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes()) - axisStyle.WriteToRenderer(r) - - cursor := canvasBox.Top - for _, bar := range sbc.Bars { - if len(bar.Name) > 0 { - barLabelBox := Box{ - Top: cursor, - Left: 0, - Right: canvasBox.Left + DefaultYAxisMargin, - Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(), - } - lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle) - linesBox := Text.MeasureLines(r, lines, axisStyle) - - yAxisWidth = MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), yAxisWidth) - } - } - return Box{ - Top: canvasBox.Top, - Left: canvasBox.Left + yAxisWidth, - Right: canvasBox.Right, - Bottom: canvasBox.Top + totalHeight, - } - } - return Box{ - Top: canvasBox.Top, - Left: canvasBox.Left, - Right: canvasBox.Right, - Bottom: canvasBox.Top + totalHeight, - } -} - // Box returns the chart bounds as a box. func (sbc StackedBarChart) Box() Box { dpr := sbc.Background.Padding.GetRight(10) dpb := sbc.Background.Padding.GetBottom(50) return Box{ - Top: sbc.Background.Padding.GetTop(20), - Left: sbc.Background.Padding.GetLeft(20), + Top: 20, + Left: 20, Right: sbc.GetWidth() - dpr, Bottom: sbc.GetHeight() - dpb, } @@ -537,12 +288,9 @@ func (sbc StackedBarChart) Box() Box { func (sbc StackedBarChart) styleDefaultsStackedBarValue(index int) Style { return Style{ - StrokeColor: sbc.GetColorPalette().GetSeriesColor(index), + StrokeColor: GetAlternateColor(index), StrokeWidth: 3.0, - FillColor: sbc.GetColorPalette().GetSeriesColor(index), - FontSize: sbc.getScaledFontSize(), - FontColor: sbc.GetColorPalette().TextColor(), - Font: sbc.GetFont(), + FillColor: GetAlternateColor(index), } } @@ -557,22 +305,8 @@ func (sbc StackedBarChart) styleDefaultsTitle() Style { }) } -func (sbc StackedBarChart) getScaledFontSize() float64 { - effectiveDimension := MinInt(sbc.GetWidth(), sbc.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 (sbc StackedBarChart) getTitleFontSize() float64 { - effectiveDimension := MinInt(sbc.GetWidth(), sbc.GetHeight()) + effectiveDimension := util.Math.MinInt(sbc.GetWidth(), sbc.GetHeight()) if effectiveDimension >= 2048 { return 48 } else if effectiveDimension >= 1024 { @@ -596,19 +330,6 @@ func (sbc StackedBarChart) styleDefaultsAxes() Style { TextWrap: TextWrapWord, } } - -func (sbc StackedBarChart) styleDefaultsHorizontalAxes() Style { - return Style{ - StrokeColor: DefaultAxisColor, - Font: sbc.GetFont(), - FontSize: DefaultAxisFontSize, - FontColor: DefaultAxisColor, - TextHorizontalAlign: TextHorizontalAlignCenter, - TextVerticalAlign: TextVerticalAlignMiddle, - TextWrap: TextWrapWord, - } -} - func (sbc StackedBarChart) styleDefaultsElements() Style { return Style{ Font: sbc.GetFont(), diff --git a/stringutil.go b/stringutil.go deleted file mode 100644 index 858d447..0000000 --- a/stringutil.go +++ /dev/null @@ -1,57 +0,0 @@ -package chart - -import "strings" - -// SplitCSV splits a corpus by the `,`, dropping leading or trailing whitespace unless quoted. -func SplitCSV(text string) (output []string) { - if len(text) == 0 { - return - } - - var state int - var word []rune - var opened rune - for _, r := range text { - switch state { - case 0: // word - if isQuote(r) { - opened = r - state = 1 - } else if isCSVDelim(r) { - output = append(output, strings.TrimSpace(string(word))) - word = nil - } else { - word = append(word, r) - } - case 1: // we're in a quoted section - if matchesQuote(opened, r) { - state = 0 - continue - } - word = append(word, r) - } - } - - if len(word) > 0 { - output = append(output, strings.TrimSpace(string(word))) - } - return -} - -func isCSVDelim(r rune) bool { - return r == rune(',') -} - -func isQuote(r rune) bool { - return r == '"' || r == '\'' || r == '“' || r == '”' || r == '`' -} - -func matchesQuote(a, b rune) bool { - if a == '“' && b == '”' { - return true - } - if a == '”' && b == '“' { - return true - } - return a == b -} diff --git a/stringutil_test.go b/stringutil_test.go deleted file mode 100644 index 56a60d9..0000000 --- a/stringutil_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestSplitCSV(t *testing.T) { - // replaced new assertions helper - - testutil.AssertEmpty(t, SplitCSV("")) - testutil.AssertEqual(t, []string{"foo"}, SplitCSV("foo")) - testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV("foo,bar")) - testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV("foo, bar")) - testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV(" foo , bar ")) - testutil.AssertEqual(t, []string{"foo", "bar", "baz"}, SplitCSV("foo,bar,baz")) - testutil.AssertEqual(t, []string{"foo", "bar", "baz,buzz"}, SplitCSV("foo,bar,\"baz,buzz\"")) - testutil.AssertEqual(t, []string{"foo", "bar", "baz,'buzz'"}, SplitCSV("foo,bar,\"baz,'buzz'\"")) - testutil.AssertEqual(t, []string{"foo", "bar", "baz,'buzz"}, SplitCSV("foo,bar,\"baz,'buzz\"")) - testutil.AssertEqual(t, []string{"foo", "bar", "baz,\"buzz\""}, SplitCSV("foo,bar,'baz,\"buzz\"'")) -} diff --git a/style.go b/style.go index 6816cf4..b1a145d 100644 --- a/style.go +++ b/style.go @@ -4,8 +4,9 @@ import ( "fmt" "strings" - "git.smarteching.com/zeni/go-chart/v2/drawing" + util "github.com/blendlabs/go-util" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" ) const ( @@ -14,18 +15,10 @@ const ( Disabled = -1 ) -// Hidden is a prebuilt style with the `Hidden` property set to true. -func Hidden() Style { +// StyleShow is a prebuilt style with the `Show` property set to true. +func StyleShow() Style { return Style{ - Hidden: true, - } -} - -// Shown is a prebuilt style with the `Hidden` property set to false. -// You can also think of this as the default. -func Shown() Style { - return Style{ - Hidden: false, + Show: true, } } @@ -34,7 +27,7 @@ func Shown() Style { func StyleTextDefaults() Style { font, _ := GetDefaultFont() return Style{ - Hidden: false, + Show: true, Font: font, FontColor: DefaultTextColor, FontSize: DefaultTitleFontSize, @@ -43,11 +36,9 @@ func StyleTextDefaults() Style { // Style is a simple style set. type Style struct { - Hidden bool + Show bool Padding Box - ClassName string - StrokeWidth float64 StrokeColor drawing.Color StrokeDashArray []float64 @@ -73,16 +64,14 @@ type Style struct { // IsZero returns if the object is set or not. func (s Style) IsZero() bool { - return !s.Hidden && - s.StrokeColor.IsZero() && + return s.StrokeColor.IsZero() && s.StrokeWidth == 0 && s.DotColor.IsZero() && s.DotWidth == 0 && s.FillColor.IsZero() && s.FontColor.IsZero() && s.FontSize == 0 && - s.Font == nil && - s.ClassName == "" + s.Font == nil } // String returns a text representation of the style. @@ -92,16 +81,10 @@ func (s Style) String() string { } var output []string - if s.Hidden { - output = []string{"\"hidden\": true"} + if s.Show { + output = []string{"\"show\": true"} } else { - output = []string{"\"hidden\": false"} - } - - if s.ClassName != "" { - output = append(output, fmt.Sprintf("\"class_name\": %s", s.ClassName)) - } else { - output = append(output, "\"class_name\": null") + output = []string{"\"show\": false"} } if !s.Padding.IsZero() { @@ -172,17 +155,6 @@ func (s Style) String() string { return "{" + strings.Join(output, ", ") + "}" } -// GetClassName returns the class name or a default. -func (s Style) GetClassName(defaults ...string) string { - if s.ClassName == "" { - if len(defaults) > 0 { - return defaults[0] - } - return "" - } - return s.ClassName -} - // GetStrokeColor returns the stroke color. func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color { if s.StrokeColor.IsZero() { @@ -349,7 +321,6 @@ func (s Style) GetTextRotationDegrees(defaults ...float64) float64 { // WriteToRenderer passes the style's options to a renderer. func (s Style) WriteToRenderer(r Renderer) { - r.SetClassName(s.GetClassName()) r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeDashArray(s.GetStrokeDashArray()) @@ -360,13 +331,12 @@ func (s Style) WriteToRenderer(r Renderer) { r.ClearTextRotation() if s.GetTextRotationDegrees() != 0 { - r.SetTextRotation(DegreesToRadians(s.GetTextRotationDegrees())) + r.SetTextRotation(util.Math.DegreesToRadians(s.GetTextRotationDegrees())) } } // WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer. func (s Style) WriteDrawingOptionsToRenderer(r Renderer) { - r.SetClassName(s.GetClassName()) r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeDashArray(s.GetStrokeDashArray()) @@ -375,7 +345,6 @@ func (s Style) WriteDrawingOptionsToRenderer(r Renderer) { // WriteTextOptionsToRenderer passes just the text style options to a renderer. func (s Style) WriteTextOptionsToRenderer(r Renderer) { - r.SetClassName(s.GetClassName()) r.SetFont(s.GetFont()) r.SetFontColor(s.GetFontColor()) r.SetFontSize(s.GetFontSize()) @@ -383,8 +352,6 @@ func (s Style) WriteTextOptionsToRenderer(r Renderer) { // InheritFrom coalesces two styles into a new style. func (s Style) InheritFrom(defaults Style) (final Style) { - final.ClassName = s.GetClassName(defaults.ClassName) - final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray) @@ -412,7 +379,6 @@ func (s Style) InheritFrom(defaults Style) (final Style) { // GetStrokeOptions returns the stroke components. func (s Style) GetStrokeOptions() Style { return Style{ - ClassName: s.ClassName, StrokeDashArray: s.StrokeDashArray, StrokeColor: s.StrokeColor, StrokeWidth: s.StrokeWidth, @@ -422,7 +388,6 @@ func (s Style) GetStrokeOptions() Style { // GetFillOptions returns the fill components. func (s Style) GetFillOptions() Style { return Style{ - ClassName: s.ClassName, FillColor: s.FillColor, } } @@ -430,7 +395,6 @@ func (s Style) GetFillOptions() Style { // GetDotOptions returns the dot components. func (s Style) GetDotOptions() Style { return Style{ - ClassName: s.ClassName, StrokeDashArray: nil, FillColor: s.DotColor, StrokeColor: s.DotColor, @@ -441,7 +405,6 @@ func (s Style) GetDotOptions() Style { // GetFillAndStrokeOptions returns the fill and stroke components. func (s Style) GetFillAndStrokeOptions() Style { return Style{ - ClassName: s.ClassName, StrokeDashArray: s.StrokeDashArray, FillColor: s.FillColor, StrokeColor: s.StrokeColor, @@ -452,7 +415,6 @@ func (s Style) GetFillAndStrokeOptions() Style { // GetTextOptions returns just the text components of the style. func (s Style) GetTextOptions() Style { return Style{ - ClassName: s.ClassName, FontColor: s.FontColor, FontSize: s.FontSize, Font: s.Font, diff --git a/style_test.go b/style_test.go index 5568a92..4fe8303 100644 --- a/style_test.go +++ b/style_test.go @@ -3,121 +3,121 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/drawing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" ) func TestStyleIsZero(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) zero := Style{} - testutil.AssertTrue(t, zero.IsZero()) + assert.True(zero.IsZero()) strokeColor := Style{StrokeColor: drawing.ColorWhite} - testutil.AssertFalse(t, strokeColor.IsZero()) + assert.False(strokeColor.IsZero()) fillColor := Style{FillColor: drawing.ColorWhite} - testutil.AssertFalse(t, fillColor.IsZero()) + assert.False(fillColor.IsZero()) strokeWidth := Style{StrokeWidth: 5.0} - testutil.AssertFalse(t, strokeWidth.IsZero()) + assert.False(strokeWidth.IsZero()) fontSize := Style{FontSize: 12.0} - testutil.AssertFalse(t, fontSize.IsZero()) + assert.False(fontSize.IsZero()) fontColor := Style{FontColor: drawing.ColorWhite} - testutil.AssertFalse(t, fontColor.IsZero()) + assert.False(fontColor.IsZero()) font := Style{Font: &truetype.Font{}} - testutil.AssertFalse(t, font.IsZero()) + assert.False(font.IsZero()) } func TestStyleGetStrokeColor(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertEqual(t, drawing.ColorTransparent, unset.GetStrokeColor()) - testutil.AssertEqual(t, drawing.ColorWhite, unset.GetStrokeColor(drawing.ColorWhite)) + assert.Equal(drawing.ColorTransparent, unset.GetStrokeColor()) + assert.Equal(drawing.ColorWhite, unset.GetStrokeColor(drawing.ColorWhite)) set := Style{StrokeColor: drawing.ColorWhite} - testutil.AssertEqual(t, drawing.ColorWhite, set.GetStrokeColor()) - testutil.AssertEqual(t, drawing.ColorWhite, set.GetStrokeColor(drawing.ColorBlack)) + assert.Equal(drawing.ColorWhite, set.GetStrokeColor()) + assert.Equal(drawing.ColorWhite, set.GetStrokeColor(drawing.ColorBlack)) } func TestStyleGetFillColor(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertEqual(t, drawing.ColorTransparent, unset.GetFillColor()) - testutil.AssertEqual(t, drawing.ColorWhite, unset.GetFillColor(drawing.ColorWhite)) + assert.Equal(drawing.ColorTransparent, unset.GetFillColor()) + assert.Equal(drawing.ColorWhite, unset.GetFillColor(drawing.ColorWhite)) set := Style{FillColor: drawing.ColorWhite} - testutil.AssertEqual(t, drawing.ColorWhite, set.GetFillColor()) - testutil.AssertEqual(t, drawing.ColorWhite, set.GetFillColor(drawing.ColorBlack)) + assert.Equal(drawing.ColorWhite, set.GetFillColor()) + assert.Equal(drawing.ColorWhite, set.GetFillColor(drawing.ColorBlack)) } func TestStyleGetStrokeWidth(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertEqual(t, DefaultStrokeWidth, unset.GetStrokeWidth()) - testutil.AssertEqual(t, DefaultStrokeWidth+1, unset.GetStrokeWidth(DefaultStrokeWidth+1)) + assert.Equal(DefaultStrokeWidth, unset.GetStrokeWidth()) + assert.Equal(DefaultStrokeWidth+1, unset.GetStrokeWidth(DefaultStrokeWidth+1)) set := Style{StrokeWidth: DefaultStrokeWidth + 2} - testutil.AssertEqual(t, DefaultStrokeWidth+2, set.GetStrokeWidth()) - testutil.AssertEqual(t, DefaultStrokeWidth+2, set.GetStrokeWidth(DefaultStrokeWidth+1)) + assert.Equal(DefaultStrokeWidth+2, set.GetStrokeWidth()) + assert.Equal(DefaultStrokeWidth+2, set.GetStrokeWidth(DefaultStrokeWidth+1)) } func TestStyleGetFontSize(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertEqual(t, DefaultFontSize, unset.GetFontSize()) - testutil.AssertEqual(t, DefaultFontSize+1, unset.GetFontSize(DefaultFontSize+1)) + assert.Equal(DefaultFontSize, unset.GetFontSize()) + assert.Equal(DefaultFontSize+1, unset.GetFontSize(DefaultFontSize+1)) set := Style{FontSize: DefaultFontSize + 2} - testutil.AssertEqual(t, DefaultFontSize+2, set.GetFontSize()) - testutil.AssertEqual(t, DefaultFontSize+2, set.GetFontSize(DefaultFontSize+1)) + assert.Equal(DefaultFontSize+2, set.GetFontSize()) + assert.Equal(DefaultFontSize+2, set.GetFontSize(DefaultFontSize+1)) } func TestStyleGetFontColor(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertEqual(t, drawing.ColorTransparent, unset.GetFontColor()) - testutil.AssertEqual(t, drawing.ColorWhite, unset.GetFontColor(drawing.ColorWhite)) + assert.Equal(drawing.ColorTransparent, unset.GetFontColor()) + assert.Equal(drawing.ColorWhite, unset.GetFontColor(drawing.ColorWhite)) set := Style{FontColor: drawing.ColorWhite} - testutil.AssertEqual(t, drawing.ColorWhite, set.GetFontColor()) - testutil.AssertEqual(t, drawing.ColorWhite, set.GetFontColor(drawing.ColorBlack)) + assert.Equal(drawing.ColorWhite, set.GetFontColor()) + assert.Equal(drawing.ColorWhite, set.GetFontColor(drawing.ColorBlack)) } func TestStyleGetFont(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) unset := Style{} - testutil.AssertNil(t, unset.GetFont()) - testutil.AssertEqual(t, f, unset.GetFont(f)) + assert.Nil(unset.GetFont()) + assert.Equal(f, unset.GetFont(f)) set := Style{Font: f} - testutil.AssertNotNil(t, set.GetFont()) + assert.NotNil(set.GetFont()) } func TestStyleGetPadding(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) unset := Style{} - testutil.AssertTrue(t, unset.GetPadding().IsZero()) - testutil.AssertFalse(t, unset.GetPadding(DefaultBackgroundPadding).IsZero()) - testutil.AssertEqual(t, DefaultBackgroundPadding, unset.GetPadding(DefaultBackgroundPadding)) + assert.True(unset.GetPadding().IsZero()) + assert.False(unset.GetPadding(DefaultBackgroundPadding).IsZero()) + assert.Equal(DefaultBackgroundPadding, unset.GetPadding(DefaultBackgroundPadding)) set := Style{Padding: DefaultBackgroundPadding} - testutil.AssertFalse(t, set.GetPadding().IsZero()) - testutil.AssertEqual(t, DefaultBackgroundPadding, set.GetPadding()) - testutil.AssertEqual(t, DefaultBackgroundPadding, set.GetPadding(Box{ + assert.False(set.GetPadding().IsZero()) + assert.Equal(DefaultBackgroundPadding, set.GetPadding()) + assert.Equal(DefaultBackgroundPadding, set.GetPadding(Box{ Top: DefaultBackgroundPadding.Top + 1, Left: DefaultBackgroundPadding.Left + 1, Right: DefaultBackgroundPadding.Right + 1, @@ -126,10 +126,10 @@ func TestStyleGetPadding(t *testing.T) { } func TestStyleWithDefaultsFrom(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) unset := Style{} set := Style{ @@ -142,11 +142,11 @@ func TestStyleWithDefaultsFrom(t *testing.T) { } coalesced := unset.InheritFrom(set) - testutil.AssertEqual(t, set, coalesced) + assert.Equal(set, coalesced) } func TestStyleGetStrokeOptions(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) set := Style{ StrokeColor: drawing.ColorWhite, @@ -156,14 +156,14 @@ func TestStyleGetStrokeOptions(t *testing.T) { Padding: DefaultBackgroundPadding, } svgStroke := set.GetStrokeOptions() - testutil.AssertFalse(t, svgStroke.StrokeColor.IsZero()) - testutil.AssertNotZero(t, svgStroke.StrokeWidth) - testutil.AssertTrue(t, svgStroke.FillColor.IsZero()) - testutil.AssertTrue(t, svgStroke.FontColor.IsZero()) + assert.False(svgStroke.StrokeColor.IsZero()) + assert.NotZero(svgStroke.StrokeWidth) + assert.True(svgStroke.FillColor.IsZero()) + assert.True(svgStroke.FontColor.IsZero()) } func TestStyleGetFillOptions(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) set := Style{ StrokeColor: drawing.ColorWhite, @@ -173,14 +173,14 @@ func TestStyleGetFillOptions(t *testing.T) { Padding: DefaultBackgroundPadding, } svgFill := set.GetFillOptions() - testutil.AssertFalse(t, svgFill.FillColor.IsZero()) - testutil.AssertZero(t, svgFill.StrokeWidth) - testutil.AssertTrue(t, svgFill.StrokeColor.IsZero()) - testutil.AssertTrue(t, svgFill.FontColor.IsZero()) + assert.False(svgFill.FillColor.IsZero()) + assert.Zero(svgFill.StrokeWidth) + assert.True(svgFill.StrokeColor.IsZero()) + assert.True(svgFill.FontColor.IsZero()) } func TestStyleGetFillAndStrokeOptions(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) set := Style{ StrokeColor: drawing.ColorWhite, @@ -190,14 +190,14 @@ func TestStyleGetFillAndStrokeOptions(t *testing.T) { Padding: DefaultBackgroundPadding, } svgFillAndStroke := set.GetFillAndStrokeOptions() - testutil.AssertFalse(t, svgFillAndStroke.FillColor.IsZero()) - testutil.AssertNotZero(t, svgFillAndStroke.StrokeWidth) - testutil.AssertFalse(t, svgFillAndStroke.StrokeColor.IsZero()) - testutil.AssertTrue(t, svgFillAndStroke.FontColor.IsZero()) + assert.False(svgFillAndStroke.FillColor.IsZero()) + assert.NotZero(svgFillAndStroke.StrokeWidth) + assert.False(svgFillAndStroke.StrokeColor.IsZero()) + assert.True(svgFillAndStroke.FontColor.IsZero()) } func TestStyleGetTextOptions(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) set := Style{ StrokeColor: drawing.ColorWhite, @@ -207,8 +207,8 @@ func TestStyleGetTextOptions(t *testing.T) { Padding: DefaultBackgroundPadding, } svgStroke := set.GetTextOptions() - testutil.AssertTrue(t, svgStroke.StrokeColor.IsZero()) - testutil.AssertZero(t, svgStroke.StrokeWidth) - testutil.AssertTrue(t, svgStroke.FillColor.IsZero()) - testutil.AssertFalse(t, svgStroke.FontColor.IsZero()) + assert.True(svgStroke.StrokeColor.IsZero()) + assert.Zero(svgStroke.StrokeWidth) + assert.True(svgStroke.FillColor.IsZero()) + assert.False(svgStroke.FontColor.IsZero()) } diff --git a/testutil/helpers.go b/testutil/helpers.go deleted file mode 100644 index 899d4e5..0000000 --- a/testutil/helpers.go +++ /dev/null @@ -1,216 +0,0 @@ -package testutil - -import ( - "math" - "reflect" - "strings" - "testing" -) - -// AssertNil asserts an argument is nil. -func AssertNil(t *testing.T, actual interface{}) { - t.Helper() - if !isNil(actual) { - t.Errorf("assertion failed; expected actual to be nil") - t.FailNow() - } -} - -// AssertNotNil asserts an argument is not nil. -func AssertNotNil(t *testing.T, actual interface{}, message ...interface{}) { - t.Helper() - if isNil(actual) { - t.Error("assertion failed; expected actual to not be nil") - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertEqual asserts two arguments are equal. -func AssertEqual(t *testing.T, expected, actual interface{}, message ...interface{}) { - t.Helper() - if !equal(expected, actual) { - t.Errorf("assertion failed; expected %v to equal %v", actual, expected) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertNotEqual asserts two arguments are not equal. -func AssertNotEqual(t *testing.T, expected, actual interface{}, message ...interface{}) { - t.Helper() - if equal(expected, actual) { - t.Errorf("assertion failed; expected %v to not equal %v", actual, expected) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertZero asserts an argument is zero. -func AssertZero(t *testing.T, actual interface{}, message ...interface{}) { - t.Helper() - AssertEqual(t, 0, actual) -} - -// AssertNotZero asserts an argument is not zero. -func AssertNotZero(t *testing.T, actual interface{}, message ...interface{}) { - t.Helper() - AssertNotEqual(t, 0, actual) -} - -// AssertTrue asserts an argument is true. -func AssertTrue(t *testing.T, arg bool, message ...interface{}) { - t.Helper() - if !arg { - t.Errorf("assertion failed; expected actual to be true") - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertFalse asserts an argument is false. -func AssertFalse(t *testing.T, arg bool, message ...interface{}) { - t.Helper() - if arg { - t.Errorf("assertion failed; expected actual to be false") - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertInDelta asserts a two arguments are within a delta of eachother. -// -// This delta will be determined absolute, and the delta should always be positive. -func AssertInDelta(t *testing.T, from, to, delta float64, message ...interface{}) { - t.Helper() - if diff := math.Abs(from - to); diff > delta { - t.Errorf("assertion failed; expected absolute difference of %f and %f to be %f", from, to, delta) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertEmpty asserts an argument is empty. -func AssertEmpty(t *testing.T, arg interface{}, message ...interface{}) { - t.Helper() - if getLength(arg) != 0 { - t.Errorf("assertion failed; expected actual to be empty") - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertNotEmpty asserts an argument is not empty. -func AssertNotEmpty(t *testing.T, arg interface{}, message ...interface{}) { - t.Helper() - if getLength(arg) == 0 { - t.Errorf("assertion failed; expected actual to not be empty") - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertLen asserts an argument has a given length. -func AssertLen(t *testing.T, arg interface{}, length int, message ...interface{}) { - t.Helper() - if getLength(arg) != length { - t.Errorf("assertion failed; expected actual to have length %d", length) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertContains asserts an argument contains a given substring. -func AssertContains(t *testing.T, s, substr string, message ...interface{}) { - t.Helper() - if !strings.Contains(s, substr) { - t.Errorf("assertion failed; expected actual to contain %q", substr) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -// AssertNotContains asserts an argument does not contain a given substring. -func AssertNotContains(t *testing.T, s, substr string, message ...interface{}) { - t.Helper() - if strings.Contains(s, substr) { - t.Errorf("assertion failed; expected actual to not contain %q", substr) - if len(message) > 0 { - t.Error(message...) - } - t.FailNow() - } -} - -func equal(expected, actual interface{}) bool { - if expected == nil && actual == nil { - return true - } - if (expected == nil && actual != nil) || (expected != nil && actual == nil) { - return false - } - - actualType := reflect.TypeOf(actual) - if actualType == nil { - return false - } - expectedValue := reflect.ValueOf(expected) - if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { - return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) - } - - return reflect.DeepEqual(expected, actual) -} - -func isNil(object interface{}) bool { - if object == nil { - return true - } - - value := reflect.ValueOf(object) - kind := value.Kind() - if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { - return true - } - return false -} - -func getLength(object interface{}) int { - if object == nil { - return 0 - } else if object == "" { - return 0 - } - - objValue := reflect.ValueOf(object) - - switch objValue.Kind() { - case reflect.Map: - fallthrough - case reflect.Slice, reflect.Chan, reflect.String: - { - return objValue.Len() - } - } - return 0 -} diff --git a/text.go b/text.go index 0a9dfd0..e855374 100644 --- a/text.go +++ b/text.go @@ -2,6 +2,8 @@ package chart import ( "strings" + + util "github.com/wcharczuk/go-chart/util" ) // TextHorizontalAlign is an enum for the horizontal alignment options. @@ -46,7 +48,7 @@ const ( TextVerticalAlignBottom TextVerticalAlign = 2 // TextVerticalAlignMiddle aligns the text so that there is an equal amount of space above and below the top and bottom of the ligatures. TextVerticalAlignMiddle TextVerticalAlign = 3 - // TextVerticalAlignMiddleBaseline aligns the text vertically so that there is an equal number of pixels above and below the baseline of the string. + // TextVerticalAlignMiddleBaseline aligns the text veritcally so that there is an equal number of pixels above and below the baseline of the string. TextVerticalAlignMiddleBaseline TextVerticalAlign = 4 // TextVerticalAlignTop alignts the text so that the top of the ligatures are at y-pixel 0 in the container. TextVerticalAlignTop TextVerticalAlign = 5 @@ -83,7 +85,7 @@ func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []st var line string var word string - var textBox Box + var textBox Box2d for _, c := range value { if c == rune('\n') { // commit the line to output @@ -95,7 +97,7 @@ func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []st textBox = r.MeasureText(line + word + string(c)) - if textBox.Width() >= width { + if int(textBox.Width()) >= width { output = append(output, t.Trim(line)) line = word word = string(c) @@ -118,7 +120,7 @@ func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []st var output []string var line string - var textBox Box + var textBox Box2d for _, c := range value { if c == rune('\n') { output = append(output, line) @@ -128,7 +130,7 @@ func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []st textBox = r.MeasureText(line + string(c)) - if textBox.Width() >= width { + if int(textBox.Width()) >= width { output = append(output, line) line = string(c) continue @@ -142,18 +144,18 @@ func (t text) Trim(value string) string { return strings.Trim(value, " \t\n\r") } -func (t text) MeasureLines(r Renderer, lines []string, style Style) Box { +func (t text) MeasureLines(r Renderer, lines []string, style Style) Box2d { style.WriteTextOptionsToRenderer(r) var output Box for index, line := range lines { lineBox := r.MeasureText(line) - output.Right = MaxInt(lineBox.Right, output.Right) - output.Bottom += lineBox.Height() + output.Right = util.Math.MaxInt(int(lineBox.Right()), output.Right) + output.Bottom += int(lineBox.Height()) if index < len(lines)-1 { output.Bottom += +style.GetTextLineSpacing() } } - return output + return output.Corners() } func (t text) appendLast(lines []string, text string) []string { diff --git a/text_test.go b/text_test.go index 54a0181..78c0e9b 100644 --- a/text_test.go +++ b/text_test.go @@ -3,58 +3,58 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestTextWrapWord(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) basicTextStyle := Style{Font: f, FontSize: 24} output := Text.WrapFitWord(r, "this is a test string", 100, basicTextStyle) - testutil.AssertNotEmpty(t, output) - testutil.AssertLen(t, output, 3) + assert.NotEmpty(output) + assert.Len(output, 3) for _, line := range output { basicTextStyle.WriteToRenderer(r) lineBox := r.MeasureText(line) - testutil.AssertTrue(t, lineBox.Width() < 100, line+": "+lineBox.String()) + assert.True(lineBox.Width() < 100, line+": "+lineBox.String()) } - testutil.AssertEqual(t, "this is", output[0]) - testutil.AssertEqual(t, "a test", output[1]) - testutil.AssertEqual(t, "string", output[2]) + assert.Equal("this is", output[0]) + assert.Equal("a test", output[1]) + assert.Equal("string", output[2]) output = Text.WrapFitWord(r, "foo", 100, basicTextStyle) - testutil.AssertLen(t, output, 1) - testutil.AssertEqual(t, "foo", output[0]) + assert.Len(output, 1) + assert.Equal("foo", output[0]) // test that it handles newlines. output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring", 100, basicTextStyle) - testutil.AssertLen(t, output, 5) + assert.Len(output, 5) // test that it handles newlines and long lines. output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring that is very long", 100, basicTextStyle) - testutil.AssertLen(t, output, 8) + assert.Len(output, 8) } func TestTextWrapRune(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) basicTextStyle := Style{Font: f, FontSize: 24} output := Text.WrapFitRune(r, "this is a test string", 150, basicTextStyle) - testutil.AssertNotEmpty(t, output) - testutil.AssertLen(t, output, 2) - testutil.AssertEqual(t, "this is a t", output[0]) - testutil.AssertEqual(t, "est string", output[1]) + assert.NotEmpty(output) + assert.Len(output, 2) + assert.Equal("this is a t", output[0]) + assert.Equal("est string", output[1]) } diff --git a/tick.go b/tick.go index 1732c60..875fc5f 100644 --- a/tick.go +++ b/tick.go @@ -4,6 +4,8 @@ import ( "fmt" "math" "strings" + + util "github.com/wcharczuk/go-chart/util" ) // TicksProvider is a type that provides ticks. @@ -83,15 +85,15 @@ func GenerateContinuousTicks(r Renderer, ra Range, isVertical bool, style Style, rangeDelta := math.Abs(max - min) tickStep := rangeDelta / float64(intermediateTickCount) - roundTo := GetRoundToForDelta(rangeDelta) / 10 - intermediateTickCount = MinInt(intermediateTickCount, DefaultTickCountSanityCheck) + roundTo := util.Math.GetRoundToForDelta(rangeDelta) / 10 + intermediateTickCount = util.Math.MinInt(intermediateTickCount, 1<<10) for x := 1; x < intermediateTickCount; x++ { var tickValue float64 if ra.IsDescending() { - tickValue = max - RoundUp(tickStep*float64(x), roundTo) + tickValue = max - util.Math.RoundUp(tickStep*float64(x), roundTo) } else { - tickValue = min + RoundUp(tickStep*float64(x), roundTo) + tickValue = min + util.Math.RoundUp(tickStep*float64(x), roundTo) } ticks = append(ticks, Tick{ Value: tickValue, diff --git a/tick_test.go b/tick_test.go index 0c28d99..7cea3c9 100644 --- a/tick_test.go +++ b/tick_test.go @@ -3,17 +3,17 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestGenerateContinuousTicks(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) r.SetFont(f) ra := &ContinuousRange{ @@ -25,20 +25,20 @@ func TestGenerateContinuousTicks(t *testing.T) { vf := FloatValueFormatter ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) - testutil.AssertNotEmpty(t, ticks) - testutil.AssertLen(t, ticks, 11) - testutil.AssertEqual(t, 0.0, ticks[0].Value) - testutil.AssertEqual(t, 10, ticks[len(ticks)-1].Value) + assert.NotEmpty(ticks) + assert.Len(ticks, 11) + assert.Equal(0.0, ticks[0].Value) + assert.Equal(10, ticks[len(ticks)-1].Value) } func TestGenerateContinuousTicksDescending(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) r.SetFont(f) ra := &ContinuousRange{ @@ -51,10 +51,10 @@ func TestGenerateContinuousTicksDescending(t *testing.T) { vf := FloatValueFormatter ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) - testutil.AssertNotEmpty(t, ticks) - testutil.AssertLen(t, ticks, 11) - testutil.AssertEqual(t, 10.0, ticks[0].Value) - testutil.AssertEqual(t, 9.0, ticks[1].Value) - testutil.AssertEqual(t, 1.0, ticks[len(ticks)-2].Value) - testutil.AssertEqual(t, 0.0, ticks[len(ticks)-1].Value) + assert.NotEmpty(ticks) + assert.Len(ticks, 11) + assert.Equal(10.0, ticks[0].Value) + assert.Equal(9.0, ticks[1].Value) + assert.Equal(1.0, ticks[len(ticks)-2].Value) + assert.Equal(0.0, ticks[len(ticks)-1].Value) } diff --git a/time_series.go b/time_series.go index 83ee905..d2636a1 100644 --- a/time_series.go +++ b/time_series.go @@ -3,14 +3,8 @@ package chart import ( "fmt" "time" -) -// Interface Assertions. -var ( - _ Series = (*TimeSeries)(nil) - _ FirstValuesProvider = (*TimeSeries)(nil) - _ LastValuesProvider = (*TimeSeries)(nil) - _ ValueFormatterProvider = (*TimeSeries)(nil) + util "github.com/wcharczuk/go-chart/util" ) // TimeSeries is a line on a chart. @@ -39,23 +33,16 @@ func (ts TimeSeries) Len() int { return len(ts.XValues) } -// GetValues gets x, y values at a given index. +// GetValues gets a value at a given index. func (ts TimeSeries) GetValues(index int) (x, y float64) { - x = TimeToFloat64(ts.XValues[index]) + x = util.Time.ToFloat64(ts.XValues[index]) y = ts.YValues[index] return } -// GetFirstValues gets the first values. -func (ts TimeSeries) GetFirstValues() (x, y float64) { - x = TimeToFloat64(ts.XValues[0]) - y = ts.YValues[0] - return -} - -// GetLastValues gets the last values. +// GetLastValues gets the last value. func (ts TimeSeries) GetLastValues() (x, y float64) { - x = TimeToFloat64(ts.XValues[len(ts.XValues)-1]) + x = util.Time.ToFloat64(ts.XValues[len(ts.XValues)-1]) y = ts.YValues[len(ts.YValues)-1] return } diff --git a/time_series_test.go b/time_series_test.go index 142e78a..ebf14e2 100644 --- a/time_series_test.go +++ b/time_series_test.go @@ -4,11 +4,11 @@ import ( "testing" "time" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) func TestTimeSeriesGetValue(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) ts := TimeSeries{ Name: "Test", @@ -25,12 +25,12 @@ func TestTimeSeriesGetValue(t *testing.T) { } x0, y0 := ts.GetValues(0) - testutil.AssertNotZero(t, x0) - testutil.AssertEqual(t, 1.0, y0) + assert.NotZero(x0) + assert.Equal(1.0, y0) } func TestTimeSeriesValidate(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) cs := TimeSeries{ Name: "Test Series", @@ -45,7 +45,7 @@ func TestTimeSeriesValidate(t *testing.T) { 1.0, 2.0, 3.0, 4.0, 5.0, }, } - testutil.AssertNil(t, cs.Validate()) + assert.Nil(cs.Validate()) cs = TimeSeries{ Name: "Test Series", @@ -57,7 +57,7 @@ func TestTimeSeriesValidate(t *testing.T) { time.Now().AddDate(0, 0, -1), }, } - testutil.AssertNotNil(t, cs.Validate()) + assert.NotNil(cs.Validate()) cs = TimeSeries{ Name: "Test Series", @@ -65,5 +65,5 @@ func TestTimeSeriesValidate(t *testing.T) { 1.0, 2.0, 3.0, 4.0, 5.0, }, } - testutil.AssertNotNil(t, cs.Validate()) + assert.NotNil(cs.Validate()) } diff --git a/times.go b/times.go deleted file mode 100644 index 95e2acd..0000000 --- a/times.go +++ /dev/null @@ -1,46 +0,0 @@ -package chart - -import ( - "sort" - "time" -) - -// Assert types implement interfaces. -var ( - _ Sequence = (*Times)(nil) - _ sort.Interface = (*Times)(nil) -) - -// Times are an array of times. -// It wraps the array with methods that implement `seq.Provider`. -type Times []time.Time - -// Array returns the times to an array. -func (t Times) Array() []time.Time { - return []time.Time(t) -} - -// Len returns the length of the array. -func (t Times) Len() int { - return len(t) -} - -// GetValue returns a value at an index as a time. -func (t Times) GetValue(index int) float64 { - return ToFloat64(t[index]) -} - -// Swap implements sort.Interface. -func (t Times) Swap(i, j int) { - t[i], t[j] = t[j], t[i] -} - -// Less implements sort.Interface. -func (t Times) Less(i, j int) bool { - return t[i].Before(t[j]) -} - -// ToFloat64 returns a float64 representation of a time. -func ToFloat64(t time.Time) float64 { - return float64(t.UnixNano()) -} diff --git a/timeutil.go b/timeutil.go deleted file mode 100644 index aa6b9e4..0000000 --- a/timeutil.go +++ /dev/null @@ -1,150 +0,0 @@ -package chart - -import "time" - -// SecondsPerXYZ -const ( - SecondsPerHour = 60 * 60 - SecondsPerDay = 60 * 60 * 24 -) - -// TimeMillis returns a duration as a float millis. -func TimeMillis(d time.Duration) float64 { - return float64(d) / float64(time.Millisecond) -} - -// DiffHours returns the difference in hours between two times. -func DiffHours(t1, t2 time.Time) (hours int) { - t1n := t1.Unix() - t2n := t2.Unix() - var diff int64 - if t1n > t2n { - diff = t1n - t2n - } else { - diff = t2n - t1n - } - return int(diff / (SecondsPerHour)) -} - -// TimeMin returns the minimum and maximum times in a given range. -func TimeMin(times ...time.Time) (min time.Time) { - if len(times) == 0 { - return - } - min = times[0] - for index := 1; index < len(times); index++ { - if times[index].Before(min) { - min = times[index] - } - - } - return -} - -// TimeMax returns the minimum and maximum times in a given range. -func TimeMax(times ...time.Time) (max time.Time) { - if len(times) == 0 { - return - } - max = times[0] - - for index := 1; index < len(times); index++ { - if times[index].After(max) { - max = times[index] - } - } - return -} - -// TimeMinMax returns the minimum and maximum times in a given range. -func TimeMinMax(times ...time.Time) (min, max time.Time) { - if len(times) == 0 { - return - } - min = times[0] - max = times[0] - - for index := 1; index < len(times); index++ { - if times[index].Before(min) { - min = times[index] - } - if times[index].After(max) { - max = times[index] - } - } - return -} - -// TimeToFloat64 returns a float64 representation of a time. -func TimeToFloat64(t time.Time) float64 { - return float64(t.UnixNano()) -} - -// TimeFromFloat64 returns a time from a float64. -func TimeFromFloat64(tf float64) time.Time { - return time.Unix(0, int64(tf)) -} - -// TimeDescending sorts a given list of times ascending, or min to max. -type TimeDescending []time.Time - -// Len implements sort.Sorter -func (d TimeDescending) Len() int { return len(d) } - -// Swap implements sort.Sorter -func (d TimeDescending) Swap(i, j int) { d[i], d[j] = d[j], d[i] } - -// Less implements sort.Sorter -func (d TimeDescending) Less(i, j int) bool { return d[i].After(d[j]) } - -// TimeAscending sorts a given list of times ascending, or min to max. -type TimeAscending []time.Time - -// Len implements sort.Sorter -func (a TimeAscending) Len() int { return len(a) } - -// Swap implements sort.Sorter -func (a TimeAscending) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -// Less implements sort.Sorter -func (a TimeAscending) Less(i, j int) bool { return a[i].Before(a[j]) } - -// Days generates a seq of timestamps by day, from -days to today. -func Days(days int) []time.Time { - var values []time.Time - for day := days; day >= 0; day-- { - values = append(values, time.Now().AddDate(0, 0, -day)) - } - return values -} - -// Hours returns a sequence of times by the hour for a given number of hours -// after a given start. -func Hours(start time.Time, totalHours int) []time.Time { - times := make([]time.Time, totalHours) - - last := start - for i := 0; i < totalHours; i++ { - times[i] = last - last = last.Add(time.Hour) - } - - return times -} - -// HoursFilled adds zero values for the data bounded by the start and end of the xdata array. -func HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { - start, end := TimeMinMax(xdata...) - totalHours := DiffHours(start, end) - - finalTimes := Hours(start, totalHours+1) - finalValues := make([]float64, totalHours+1) - - var hoursFromStart int - for i, xd := range xdata { - hoursFromStart = DiffHours(start, xd) - finalValues[hoursFromStart] = ydata[i] - } - - return finalTimes, finalValues -} diff --git a/util/date.go b/util/date.go new file mode 100644 index 0000000..18b8603 --- /dev/null +++ b/util/date.go @@ -0,0 +1,396 @@ +package util + +import ( + "sync" + "time" +) + +const ( + // AllDaysMask is a bitmask of all the days of the week. + AllDaysMask = 1<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) +} diff --git a/util/date_posix.go b/util/date_posix.go new file mode 100644 index 0000000..1a5a80c --- /dev/null +++ b/util/date_posix.go @@ -0,0 +1,17 @@ +// +build !windows + +package util + +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 +} diff --git a/util/date_test.go b/util/date_test.go new file mode 100644 index 0000000..223ebae --- /dev/null +++ b/util/date_test.go @@ -0,0 +1,260 @@ +package util + +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 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)) +} diff --git a/util/date_windows.go b/util/date_windows.go new file mode 100644 index 0000000..c42a367 --- /dev/null +++ b/util/date_windows.go @@ -0,0 +1,17 @@ +// +build windows + +package util + +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 +} diff --git a/util/file_util.go b/util/file_util.go new file mode 100644 index 0000000..d6c561d --- /dev/null +++ b/util/file_util.go @@ -0,0 +1,57 @@ +package util + +import ( + "bufio" + "io" + "os" +) + +var ( + // File contains file utility functions + File = fileUtil{} +) + +type fileUtil struct{} + +// ReadByLines reads a file and calls the handler for each line. +func (fu fileUtil) ReadByLines(filePath string, handler func(line string) error) error { + var f *os.File + var err error + if f, err = os.Open(filePath); err == nil { + defer f.Close() + var line string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line = scanner.Text() + err = handler(line) + if err != nil { + return err + } + } + } + return err + +} + +// ReadByChunks reads a file in `chunkSize` pieces, dispatched to the handler. +func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(line []byte) error) error { + var f *os.File + var err error + if f, err = os.Open(filePath); err == nil { + defer f.Close() + + chunk := make([]byte, chunkSize) + for { + readBytes, err := f.Read(chunk) + if err == io.EOF { + break + } + readData := chunk[:readBytes] + err = handler(readData) + if err != nil { + return err + } + } + } + return err +} diff --git a/mathutil.go b/util/math.go similarity index 50% rename from mathutil.go rename to util/math.go index d1f07f9..294fb2c 100644 --- a/mathutil.go +++ b/util/math.go @@ -1,6 +1,9 @@ -package chart +package util -import "math" +import ( + "math" + "time" +) const ( _pi = math.Pi @@ -16,201 +19,68 @@ const ( _r2d = (180.0 / math.Pi) ) -// MinMax returns the minimum and maximum of a given set of values. -func MinMax(values ...float64) (min, max float64) { +var ( + // Math contains helper methods for common math operations. + Math = &mathUtil{} +) + +type mathUtil struct{} + +// Max returns the maximum value of a group of floats. +func (m mathUtil) Max(values ...float64) float64 { if len(values) == 0 { - return - } - - max = values[0] - min = values[0] - var value float64 - for index := 1; index < len(values); index++ { - value = values[index] - if value < min { - min = value - } - if value > max { - max = value - } - } - return -} - -// MinInt returns the minimum int. -func MinInt(values ...int) (min int) { - if len(values) == 0 { - return - } - - min = values[0] - var value int - for index := 1; index < len(values); index++ { - value = values[index] - if value < min { - min = value - } - } - return -} - -// MaxInt returns the maximum int. -func MaxInt(values ...int) (max int) { - if len(values) == 0 { - return - } - - max = values[0] - var value int - for index := 1; index < len(values); index++ { - value = values[index] - if value > max { - max = value - } - } - return -} - -// AbsInt returns the absolute value of an int. -func AbsInt(value int) int { - if value < 0 { - return -value - } - return value -} - -// DegreesToRadians returns degrees as radians. -func DegreesToRadians(degrees float64) float64 { - return degrees * _d2r -} - -// RadiansToDegrees translates a radian value to a degree value. -func RadiansToDegrees(value float64) float64 { - return math.Mod(value, _2pi) * _r2d -} - -// PercentToRadians converts a normalized value (0,1) to radians. -func PercentToRadians(pct float64) float64 { - return DegreesToRadians(360.0 * pct) -} - -// RadianAdd adds a delta to a base in radians. -func RadianAdd(base, delta float64) float64 { - value := base + delta - if value > _2pi { - return math.Mod(value, _2pi) - } else if value < 0 { - return math.Mod(_2pi+value, _2pi) - } - return value -} - -// DegreesAdd adds a delta to a base in radians. -func DegreesAdd(baseDegrees, deltaDegrees float64) float64 { - value := baseDegrees + deltaDegrees - if value > _2pi { - return math.Mod(value, 360.0) - } else if value < 0 { - return math.Mod(360.0+value, 360.0) - } - return value -} - -// DegreesToCompass returns the degree value in compass / clock orientation. -func DegreesToCompass(deg float64) float64 { - return DegreesAdd(deg, -90.0) -} - -// CirclePoint returns the absolute position of a circle diameter point given -// by the radius and the theta. -func CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) { - x = cx + int(radius*math.Sin(thetaRadians)) - y = cy - int(radius*math.Cos(thetaRadians)) - return -} - -// RotateCoordinate rotates a coordinate around a given center by a theta in radians. -func RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) { - tempX, tempY := float64(x-cx), float64(y-cy) - rotatedX := tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians) - rotatedY := tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians) - rx = int(rotatedX) + cx - ry = int(rotatedY) + cy - return -} - -// RoundUp rounds up to a given roundTo value. -func RoundUp(value, roundTo float64) float64 { - if roundTo < 0.000000000000001 { - return value - } - d1 := math.Ceil(value / roundTo) - return d1 * roundTo -} - -// RoundDown rounds down to a given roundTo value. -func RoundDown(value, roundTo float64) float64 { - if roundTo < 0.000000000000001 { - return value - } - d1 := math.Floor(value / roundTo) - return d1 * roundTo -} - -// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs. -// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1 -// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc. -func Normalize(values ...float64) []float64 { - var total float64 - for _, v := range values { - total += v - } - output := make([]float64, len(values)) - for x, v := range values { - output[x] = RoundDown(v/total, 0.0001) - } - return output -} - -// Mean returns the mean of a set of values -func Mean(values ...float64) float64 { - return Sum(values...) / float64(len(values)) -} - -// MeanInt returns the mean of a set of integer values. -func MeanInt(values ...int) int { - return SumInt(values...) / len(values) -} - -// Sum sums a set of values. -func Sum(values ...float64) float64 { - var total float64 - for _, v := range values { - total += v - } - return total -} - -// SumInt sums a set of values. -func SumInt(values ...int) int { - var total int - for _, v := range values { - total += v - } - return total -} - -// PercentDifference computes the percentage difference between two values. -// The formula is (v2-v1)/v1. -func PercentDifference(v1, v2 float64) float64 { - if v1 == 0 { return 0 } - return (v2 - v1) / v1 + max := values[0] + for _, v := range values { + if max < v { + max = v + } + } + return max +} + +// MinAndMax returns both the min and max in one pass. +func (m mathUtil) MinAndMax(values ...float64) (min float64, max float64) { + if len(values) == 0 { + return + } + min = values[0] + max = values[0] + for _, v := range values[1:] { + if max < v { + max = v + } + if min > v { + min = v + } + } + return +} + +// MinAndMaxOfTime returns the min and max of a given set of times +// in one pass. +func (m mathUtil) MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) { + if len(values) == 0 { + return + } + + min = values[0] + max = values[0] + + for _, v := range values[1:] { + if max.Before(v) { + max = v + } + if min.After(v) { + min = v + } + } + return } // GetRoundToForDelta returns a `roundTo` value for a given delta. -func GetRoundToForDelta(delta float64) float64 { +func (m mathUtil) GetRoundToForDelta(delta float64) float64 { startingDeltaBound := math.Pow(10.0, 10.0) for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 { if delta > cursor { @@ -221,32 +91,177 @@ func GetRoundToForDelta(delta float64) float64 { return 0.0 } -// RoundPlaces rounds an input to a given places. -func RoundPlaces(input float64, places int) (rounded float64) { - if math.IsNaN(input) { - return 0.0 - } - - sign := 1.0 - if input < 0 { - sign = -1 - input *= -1 - } - - precision := math.Pow(10, float64(places)) - digit := input * precision - _, decimal := math.Modf(digit) - - if decimal >= 0.5 { - rounded = math.Ceil(digit) - } else { - rounded = math.Floor(digit) - } - - return rounded / precision * sign +// RoundUp rounds up to a given roundTo value. +func (m mathUtil) RoundUp(value, roundTo float64) float64 { + d1 := math.Ceil(value / roundTo) + return d1 * roundTo } -func f64i(value float64) int { - r := RoundPlaces(value, 0) - return int(r) +// RoundDown rounds down to a given roundTo value. +func (m mathUtil) RoundDown(value, roundTo float64) float64 { + d1 := math.Floor(value / roundTo) + return d1 * roundTo +} + +// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs. +// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1 +// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc. +func (m mathUtil) Normalize(values ...float64) []float64 { + var total float64 + for _, v := range values { + total += v + } + output := make([]float64, len(values)) + for x, v := range values { + output[x] = m.RoundDown(v/total, 0.0001) + } + return output +} + +// MinInt returns the minimum of a set of integers. +func (m mathUtil) MinInt(values ...int) int { + min := math.MaxInt32 + for _, v := range values { + if v < min { + min = v + } + } + return min +} + +// MaxInt returns the maximum of a set of integers. +func (m mathUtil) MaxInt(values ...int) int { + max := math.MinInt32 + for _, v := range values { + if v > max { + max = v + } + } + return max +} + +// AbsInt returns the absolute value of an integer. +func (m mathUtil) AbsInt(value int) int { + if value < 0 { + return -value + } + return value +} + +// AbsInt64 returns the absolute value of a long. +func (m mathUtil) AbsInt64(value int64) int64 { + if value < 0 { + return -value + } + return value +} + +// Mean returns the mean of a set of values +func (m mathUtil) Mean(values ...float64) float64 { + return m.Sum(values...) / float64(len(values)) +} + +// MeanInt returns the mean of a set of integer values. +func (m mathUtil) MeanInt(values ...int) int { + return m.SumInt(values...) / len(values) +} + +// Sum sums a set of values. +func (m mathUtil) Sum(values ...float64) float64 { + var total float64 + for _, v := range values { + total += v + } + return total +} + +// SumInt sums a set of values. +func (m mathUtil) SumInt(values ...int) int { + var total int + for _, v := range values { + total += v + } + return total +} + +// PercentDifference computes the percentage difference between two values. +// The formula is (v2-v1)/v1. +func (m mathUtil) PercentDifference(v1, v2 float64) float64 { + if v1 == 0 { + return 0 + } + return (v2 - v1) / v1 +} + +// DegreesToRadians returns degrees as radians. +func (m mathUtil) DegreesToRadians(degrees float64) float64 { + return degrees * _d2r +} + +// RadiansToDegrees translates a radian value to a degree value. +func (m mathUtil) RadiansToDegrees(value float64) float64 { + return math.Mod(value, _2pi) * _r2d +} + +// PercentToRadians converts a normalized value (0,1) to radians. +func (m mathUtil) PercentToRadians(pct float64) float64 { + return m.DegreesToRadians(360.0 * pct) +} + +// RadianAdd adds a delta to a base in radians. +func (m mathUtil) RadianAdd(base, delta float64) float64 { + value := base + delta + if value > _2pi { + return math.Mod(value, _2pi) + } else if value < 0 { + return math.Mod(_2pi+value, _2pi) + } + return value +} + +// DegreesAdd adds a delta to a base in radians. +func (m mathUtil) DegreesAdd(baseDegrees, deltaDegrees float64) float64 { + value := baseDegrees + deltaDegrees + if value > _2pi { + return math.Mod(value, 360.0) + } else if value < 0 { + return math.Mod(360.0+value, 360.0) + } + return value +} + +// DegreesToCompass returns the degree value in compass / clock orientation. +func (m mathUtil) DegreesToCompass(deg float64) float64 { + return m.DegreesAdd(deg, -90.0) +} + +// CirclePoint returns the absolute position of a circle diameter point given +// by the radius and the theta. +func (m mathUtil) CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) { + x = cx + int(radius*math.Sin(thetaRadians)) + y = cy - int(radius*math.Cos(thetaRadians)) + return +} + +func (m mathUtil) RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) { + tempX, tempY := float64(x-cx), float64(y-cy) + rotatedX := tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians) + rotatedY := tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians) + rx = int(rotatedX) + cx + ry = int(rotatedY) + cy + return +} + +func (m mathUtil) LinesIntersect(l0x0, l0y0, l0x1, l0y1, l1x0, l1y0, l1x1, l1y1 float64) bool { + var s0x, s0y, s1x, s1y float64 + s0x = l0x1 - l0x0 + s0y = l0y1 - l0y0 + s1x = l1x1 - l1x0 + s1y = l1y1 - l1y0 + + var s, t float64 + s = (-s0y*(l0x0-l1x0) + s0x*(l0y0-l1y0)) / (-s1x*s0y + s0x*s1y) + t = (s1x*(l0y0-l1y0) - s1y*(l0x0-l1x0)) / (-s1x*s0y + s0x*s1y) + + return s >= 0 && s <= 1 && t >= 0 && t <= 1 } diff --git a/util/math_test.go b/util/math_test.go new file mode 100644 index 0000000..af44e15 --- /dev/null +++ b/util/math_test.go @@ -0,0 +1,208 @@ +package util + +import ( + "testing" + "time" + + "github.com/blendlabs/go-assert" +) + +func TestMinAndMax(t *testing.T) { + assert := assert.New(t) + values := []float64{1.0, 2.0, 3.0, 4.0} + min, max := Math.MinAndMax(values...) + assert.Equal(1.0, min) + assert.Equal(4.0, max) +} + +func TestMinAndMaxReversed(t *testing.T) { + assert := assert.New(t) + values := []float64{4.0, 2.0, 3.0, 1.0} + min, max := Math.MinAndMax(values...) + assert.Equal(1.0, min) + assert.Equal(4.0, max) +} + +func TestMinAndMaxEmpty(t *testing.T) { + assert := assert.New(t) + values := []float64{} + min, max := Math.MinAndMax(values...) + assert.Equal(0.0, min) + assert.Equal(0.0, max) +} + +func TestMinAndMaxOfTime(t *testing.T) { + assert := assert.New(t) + values := []time.Time{ + time.Now().AddDate(0, 0, -1), + time.Now().AddDate(0, 0, -2), + time.Now().AddDate(0, 0, -3), + time.Now().AddDate(0, 0, -4), + } + min, max := Math.MinAndMaxOfTime(values...) + assert.Equal(values[3], min) + assert.Equal(values[0], max) +} + +func TestMinAndMaxOfTimeReversed(t *testing.T) { + assert := assert.New(t) + values := []time.Time{ + time.Now().AddDate(0, 0, -4), + time.Now().AddDate(0, 0, -2), + time.Now().AddDate(0, 0, -3), + time.Now().AddDate(0, 0, -1), + } + min, max := Math.MinAndMaxOfTime(values...) + assert.Equal(values[0], min) + assert.Equal(values[3], max) +} + +func TestMinAndMaxOfTimeEmpty(t *testing.T) { + assert := assert.New(t) + values := []time.Time{} + min, max := Math.MinAndMaxOfTime(values...) + assert.Equal(time.Time{}, min) + assert.Equal(time.Time{}, max) +} + +func TestGetRoundToForDelta(t *testing.T) { + assert := assert.New(t) + + assert.Equal(100.0, Math.GetRoundToForDelta(1001.00)) + assert.Equal(10.0, Math.GetRoundToForDelta(101.00)) + assert.Equal(1.0, Math.GetRoundToForDelta(11.00)) +} + +func TestRoundUp(t *testing.T) { + assert := assert.New(t) + assert.Equal(0.5, Math.RoundUp(0.49, 0.1)) + assert.Equal(1.0, Math.RoundUp(0.51, 1.0)) + assert.Equal(0.4999, Math.RoundUp(0.49988, 0.0001)) +} + +func TestRoundDown(t *testing.T) { + assert := assert.New(t) + assert.Equal(0.5, Math.RoundDown(0.51, 0.1)) + assert.Equal(1.0, Math.RoundDown(1.01, 1.0)) + assert.Equal(0.5001, Math.RoundDown(0.50011, 0.0001)) +} + +func TestPercentDifference(t *testing.T) { + assert := assert.New(t) + + assert.Equal(0.5, Math.PercentDifference(1.0, 1.5)) + assert.Equal(-0.5, Math.PercentDifference(2.0, 1.0)) +} + +func TestNormalize(t *testing.T) { + assert := assert.New(t) + + values := []float64{10, 9, 8, 7, 6} + normalized := Math.Normalize(values...) + assert.Len(normalized, 5) + assert.Equal(0.25, normalized[0]) + assert.Equal(0.1499, normalized[4]) +} + +var ( + _degreesToRadians = map[float64]float64{ + 0: 0, // !_2pi b/c no irrational nums in floats. + 45: _pi4, + 90: _pi2, + 135: _3pi4, + 180: _pi, + 225: _5pi4, + 270: _3pi2, + 315: _7pi4, + } + + _compassToRadians = map[float64]float64{ + 0: _pi2, + 45: _pi4, + 90: 0, // !_2pi b/c no irrational nums in floats. + 135: _7pi4, + 180: _3pi2, + 225: _5pi4, + 270: _pi, + 315: _3pi4, + } +) + +func TestDegreesToRadians(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(r, Math.DegreesToRadians(d)) + } +} + +func TestPercentToRadians(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(r, Math.PercentToRadians(d/360.0)) + } +} + +func TestRadiansToDegrees(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(d, Math.RadiansToDegrees(r)) + } +} + +func TestRadianAdd(t *testing.T) { + assert := assert.New(t) + + assert.Equal(_pi, Math.RadianAdd(_pi2, _pi2)) + assert.Equal(_3pi2, Math.RadianAdd(_pi2, _pi)) + assert.Equal(_pi, Math.RadianAdd(_pi, _2pi)) + assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi)) +} + +func TestRotateCoordinate90(t *testing.T) { + assert := assert.New(t) + + cx, cy := 10, 10 + x, y := 5, 10 + + rx, ry := Math.RotateCoordinate(cx, cy, x, y, Math.DegreesToRadians(90)) + assert.Equal(10, rx) + assert.Equal(5, ry) +} + +func TestRotateCoordinate45(t *testing.T) { + assert := assert.New(t) + + cx, cy := 10, 10 + x, y := 5, 10 + + rx, ry := Math.RotateCoordinate(cx, cy, x, y, Math.DegreesToRadians(45)) + assert.Equal(7, rx) + assert.Equal(7, ry) +} + +func TestLinesIntersect(t *testing.T) { + assert := assert.New(t) + + p0x := 1.0 + p0y := 1.0 + + p1x := 3.0 + p1y := 1.0 + + p2x := 2.0 + p2y := 2.0 + + p3x := 2.0 + p3y := 0.0 + + p4x := 2.0 + p4y := 2.0 + p5x := 3.0 + p5y := 2.0 + + assert.True(Math.LinesIntersect(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y)) + assert.False(Math.LinesIntersect(p0x, p0y, p1x, p1y, p4x, p4y, p5x, p5y)) +} diff --git a/util/time.go b/util/time.go new file mode 100644 index 0000000..88e0c8b --- /dev/null +++ b/util/time.go @@ -0,0 +1,20 @@ +package util + +import "time" + +var ( + // Time contains time utility functions. + Time = timeUtil{} +) + +type timeUtil struct{} + +// TimeToFloat64 returns a float64 representation of a time. +func (tu timeUtil) ToFloat64(t time.Time) float64 { + return float64(t.UnixNano()) +} + +// Float64ToTime returns a time from a float64. +func (tu timeUtil) FromFloat64(tf float64) time.Time { + return time.Unix(0, int64(tf)) +} diff --git a/util/time_test.go b/util/time_test.go new file mode 100644 index 0000000..dff108b --- /dev/null +++ b/util/time_test.go @@ -0,0 +1,16 @@ +package util + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" +) + +func TestTimeFromFloat64(t *testing.T) { + assert := assert.New(t) + + now := time.Now() + + assert.InTimeDelta(now, Time.FromFloat64(Time.ToFloat64(now)), time.Microsecond) +} diff --git a/value.go b/value.go index 783e304..4b747e0 100644 --- a/value.go +++ b/value.go @@ -1,5 +1,7 @@ package chart +import util "github.com/blendlabs/go-util" + // Value is a chart value. type Value struct { Style Style @@ -21,7 +23,7 @@ func (vs Values) Values() []float64 { // ValuesNormalized returns normalized values. func (vs Values) ValuesNormalized() []float64 { - return Normalize(vs.Values()...) + return util.Math.Normalize(vs.Values()...) } // Normalize returns the values normalized. @@ -38,7 +40,7 @@ func (vs Values) Normalize() []Value { output = append(output, Value{ Style: v.Style, Label: v.Label, - Value: RoundDown(v.Value/total, 0.0001), + Value: util.Math.RoundDown(v.Value/total, 0.0001), }) } } diff --git a/value_buffer_test.go b/value_buffer_test.go deleted file mode 100644 index 1b60840..0000000 --- a/value_buffer_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package chart - -import ( - "testing" - - "git.smarteching.com/zeni/go-chart/v2/testutil" -) - -func TestBuffer(t *testing.T) { - // replaced new assertions helper - - buffer := NewValueBuffer() - - buffer.Enqueue(1) - testutil.AssertEqual(t, 1, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 1, buffer.PeekBack()) - - buffer.Enqueue(2) - testutil.AssertEqual(t, 2, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 2, buffer.PeekBack()) - - buffer.Enqueue(3) - testutil.AssertEqual(t, 3, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 3, buffer.PeekBack()) - - buffer.Enqueue(4) - testutil.AssertEqual(t, 4, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 4, buffer.PeekBack()) - - buffer.Enqueue(5) - testutil.AssertEqual(t, 5, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 5, buffer.PeekBack()) - - buffer.Enqueue(6) - testutil.AssertEqual(t, 6, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 6, buffer.PeekBack()) - - buffer.Enqueue(7) - testutil.AssertEqual(t, 7, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 7, buffer.PeekBack()) - - buffer.Enqueue(8) - testutil.AssertEqual(t, 8, buffer.Len()) - testutil.AssertEqual(t, 1, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value := buffer.Dequeue() - testutil.AssertEqual(t, 1, value) - testutil.AssertEqual(t, 7, buffer.Len()) - testutil.AssertEqual(t, 2, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 2, value) - testutil.AssertEqual(t, 6, buffer.Len()) - testutil.AssertEqual(t, 3, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 3, value) - testutil.AssertEqual(t, 5, buffer.Len()) - testutil.AssertEqual(t, 4, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 4, value) - testutil.AssertEqual(t, 4, buffer.Len()) - testutil.AssertEqual(t, 5, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 5, value) - testutil.AssertEqual(t, 3, buffer.Len()) - testutil.AssertEqual(t, 6, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 6, value) - testutil.AssertEqual(t, 2, buffer.Len()) - testutil.AssertEqual(t, 7, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 7, value) - testutil.AssertEqual(t, 1, buffer.Len()) - testutil.AssertEqual(t, 8, buffer.Peek()) - testutil.AssertEqual(t, 8, buffer.PeekBack()) - - value = buffer.Dequeue() - testutil.AssertEqual(t, 8, value) - testutil.AssertEqual(t, 0, buffer.Len()) - testutil.AssertZero(t, buffer.Peek()) - testutil.AssertZero(t, buffer.PeekBack()) -} - -func TestBufferClear(t *testing.T) { - // replaced new assertions helper - - buffer := NewValueBuffer() - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - buffer.Enqueue(1) - - testutil.AssertEqual(t, 8, buffer.Len()) - - buffer.Clear() - testutil.AssertEqual(t, 0, buffer.Len()) - testutil.AssertZero(t, buffer.Peek()) - testutil.AssertZero(t, buffer.PeekBack()) -} - -func TestBufferArray(t *testing.T) { - // replaced new assertions helper - - buffer := NewValueBuffer() - buffer.Enqueue(1) - buffer.Enqueue(2) - buffer.Enqueue(3) - buffer.Enqueue(4) - buffer.Enqueue(5) - - contents := buffer.Array() - testutil.AssertLen(t, contents, 5) - testutil.AssertEqual(t, 1, contents[0]) - testutil.AssertEqual(t, 2, contents[1]) - testutil.AssertEqual(t, 3, contents[2]) - testutil.AssertEqual(t, 4, contents[3]) - testutil.AssertEqual(t, 5, contents[4]) -} - -func TestBufferEach(t *testing.T) { - // replaced new assertions helper - - buffer := NewValueBuffer() - - for x := 1; x < 17; x++ { - buffer.Enqueue(float64(x)) - } - - called := 0 - buffer.Each(func(_ int, v float64) { - if v == float64(called+1) { - called++ - } - }) - - testutil.AssertEqual(t, 16, called) -} - -func TestNewBuffer(t *testing.T) { - // replaced new assertions helper - - empty := NewValueBuffer() - testutil.AssertNotNil(t, empty) - testutil.AssertZero(t, empty.Len()) - testutil.AssertEqual(t, bufferDefaultCapacity, empty.Capacity()) - testutil.AssertZero(t, empty.Peek()) - testutil.AssertZero(t, empty.PeekBack()) -} - -func TestNewBufferWithValues(t *testing.T) { - // replaced new assertions helper - - values := NewValueBuffer(1, 2, 3, 4, 5) - testutil.AssertNotNil(t, values) - testutil.AssertEqual(t, 5, values.Len()) - testutil.AssertEqual(t, 1, values.Peek()) - testutil.AssertEqual(t, 5, values.PeekBack()) -} - -func TestBufferGrowth(t *testing.T) { - // replaced new assertions helper - - values := NewValueBuffer(1, 2, 3, 4, 5) - for i := 0; i < 1<<10; i++ { - values.Enqueue(float64(i)) - } - - testutil.AssertEqual(t, 1<<10-1, values.PeekBack()) -} diff --git a/value_formatter.go b/value_formatter.go index 1a2002a..1c264c2 100644 --- a/value_formatter.go +++ b/value_formatter.go @@ -2,7 +2,6 @@ package chart import ( "fmt" - "strconv" "time" ) @@ -50,22 +49,6 @@ func formatTime(v interface{}, dateFormat string) string { return "" } -// IntValueFormatter is a ValueFormatter for float64. -func IntValueFormatter(v interface{}) string { - switch v.(type) { - case int: - return strconv.Itoa(v.(int)) - case int64: - return strconv.FormatInt(v.(int64), 10) - case float32: - return strconv.FormatInt(int64(v.(float32)), 10) - case float64: - return strconv.FormatInt(int64(v.(float64)), 10) - default: - return "" - } -} - // FloatValueFormatter is a ValueFormatter for float64. func FloatValueFormatter(v interface{}) string { return FloatValueFormatterWithFormat(v, DefaultFloatFormat) @@ -82,29 +65,17 @@ func PercentValueFormatter(v interface{}) string { // FloatValueFormatterWithFormat is a ValueFormatter for float64 with a given format. func FloatValueFormatterWithFormat(v interface{}, floatFormat string) string { + if typed, isTyped := v.(float64); isTyped { + return fmt.Sprintf(floatFormat, typed) + } + if typed, isTyped := v.(float32); isTyped { + return fmt.Sprintf(floatFormat, typed) + } if typed, isTyped := v.(int); isTyped { return fmt.Sprintf(floatFormat, float64(typed)) } if typed, isTyped := v.(int64); isTyped { return fmt.Sprintf(floatFormat, float64(typed)) } - if typed, isTyped := v.(float32); isTyped { - return fmt.Sprintf(floatFormat, typed) - } - if typed, isTyped := v.(float64); isTyped { - return fmt.Sprintf(floatFormat, typed) - } return "" } - -// KValueFormatter is a formatter for K values. -func KValueFormatter(k float64, vf ValueFormatter) ValueFormatter { - return func(v interface{}) string { - return fmt.Sprintf("%0.0fσ %s", k, vf(v)) - } -} - -// FloatValueFormatter is a ValueFormatter for float64, exponential notation, e.g. 1.52e+08. -func ExponentialValueFormatter(v interface{}) string { - return FloatValueFormatterWithFormat(v, "%.2e") -} diff --git a/value_formatter_test.go b/value_formatter_test.go index 46396dd..ac94060 100644 --- a/value_formatter_test.go +++ b/value_formatter_test.go @@ -4,61 +4,56 @@ import ( "testing" "time" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" + "github.com/wcharczuk/go-chart/util" ) func TestTimeValueFormatterWithFormat(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) d := time.Now() - di := TimeToFloat64(d) + di := util.Time.ToFloat64(d) df := float64(di) s := formatTime(d, DefaultDateFormat) si := formatTime(di, DefaultDateFormat) sf := formatTime(df, DefaultDateFormat) - testutil.AssertEqual(t, s, si) - testutil.AssertEqual(t, s, sf) + assert.Equal(s, si) + assert.Equal(s, sf) sd := TimeValueFormatter(d) sdi := TimeValueFormatter(di) sdf := TimeValueFormatter(df) - testutil.AssertEqual(t, s, sd) - testutil.AssertEqual(t, s, sdi) - testutil.AssertEqual(t, s, sdf) + assert.Equal(s, sd) + assert.Equal(s, sdi) + assert.Equal(s, sdf) } func TestFloatValueFormatter(t *testing.T) { - // replaced new assertions helper - testutil.AssertEqual(t, "1234.00", FloatValueFormatter(1234.00)) + assert := assert.New(t) + assert.Equal("1234.00", FloatValueFormatter(1234.00)) } func TestFloatValueFormatterWithFloat32Input(t *testing.T) { - // replaced new assertions helper - testutil.AssertEqual(t, "1234.00", FloatValueFormatter(float32(1234.00))) + assert := assert.New(t) + assert.Equal("1234.00", FloatValueFormatter(float32(1234.00))) } func TestFloatValueFormatterWithIntegerInput(t *testing.T) { - // replaced new assertions helper - testutil.AssertEqual(t, "1234.00", FloatValueFormatter(1234)) + assert := assert.New(t) + assert.Equal("1234.00", FloatValueFormatter(1234)) } func TestFloatValueFormatterWithInt64Input(t *testing.T) { - // replaced new assertions helper - testutil.AssertEqual(t, "1234.00", FloatValueFormatter(int64(1234))) + assert := assert.New(t) + assert.Equal("1234.00", FloatValueFormatter(int64(1234))) } func TestFloatValueFormatterWithFormat(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) v := 123.456 sv := FloatValueFormatterWithFormat(v, "%.3f") - testutil.AssertEqual(t, "123.456", sv) - testutil.AssertEqual(t, "123.000", FloatValueFormatterWithFormat(123, "%.3f")) -} - -func TestExponentialValueFormatter(t *testing.T) { - testutil.AssertEqual(t, "1.23e+02", ExponentialValueFormatter(123.456)) - testutil.AssertEqual(t, "1.24e+07", ExponentialValueFormatter(12421243.424)) - testutil.AssertEqual(t, "4.50e-01", ExponentialValueFormatter(0.45)) + assert.Equal("123.456", sv) + assert.Equal("123.000", FloatValueFormatterWithFormat(123, "%.3f")) } diff --git a/value_provider.go b/value_provider.go index 88b719e..c141a07 100644 --- a/value_provider.go +++ b/value_provider.go @@ -1,11 +1,11 @@ package chart -import "git.smarteching.com/zeni/go-chart/v2/drawing" +import "github.com/wcharczuk/go-chart/drawing" // ValuesProvider is a type that produces values. type ValuesProvider interface { Len() int - GetValues(index int) (float64, float64) + GetValues(index int) (x float64, y float64) } // BoundedValuesProvider allows series to return a range. @@ -14,11 +14,6 @@ type BoundedValuesProvider interface { GetBoundedValues(index int) (x, y1, y2 float64) } -// FirstValuesProvider is a special type of value provider that can return it's (potentially computed) first value. -type FirstValuesProvider interface { - GetFirstValues() (x, y float64) -} - // LastValuesProvider is a special type of value provider that can return it's (potentially computed) last value. type LastValuesProvider interface { GetLastValues() (x, y float64) diff --git a/value_test.go b/value_test.go index 9108bfc..0b3b0b5 100644 --- a/value_test.go +++ b/value_test.go @@ -3,11 +3,11 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + assert "github.com/blendlabs/go-assert" ) func TestValuesValues(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) vs := []Value{ {Value: 10, Label: "Blue"}, @@ -20,18 +20,18 @@ func TestValuesValues(t *testing.T) { } values := Values(vs).Values() - testutil.AssertLen(t, values, 7) - testutil.AssertEqual(t, 10, values[0]) - testutil.AssertEqual(t, 9, values[1]) - testutil.AssertEqual(t, 8, values[2]) - testutil.AssertEqual(t, 7, values[3]) - testutil.AssertEqual(t, 6, values[4]) - testutil.AssertEqual(t, 5, values[5]) - testutil.AssertEqual(t, 2, values[6]) + assert.Len(values, 7) + assert.Equal(10, values[0]) + assert.Equal(9, values[1]) + assert.Equal(8, values[2]) + assert.Equal(7, values[3]) + assert.Equal(6, values[4]) + assert.Equal(5, values[5]) + assert.Equal(2, values[6]) } func TestValuesValuesNormalized(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) vs := []Value{ {Value: 10, Label: "Blue"}, @@ -44,13 +44,13 @@ func TestValuesValuesNormalized(t *testing.T) { } values := Values(vs).ValuesNormalized() - testutil.AssertLen(t, values, 7) - testutil.AssertEqual(t, 0.2127, values[0]) - testutil.AssertEqual(t, 0.0425, values[6]) + assert.Len(values, 7) + assert.Equal(0.2127, values[0]) + assert.Equal(0.0425, values[6]) } func TestValuesNormalize(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) vs := []Value{ {Value: 10, Label: "Blue"}, @@ -63,7 +63,7 @@ func TestValuesNormalize(t *testing.T) { } values := Values(vs).Normalize() - testutil.AssertLen(t, values, 7) - testutil.AssertEqual(t, 0.2127, values[0].Value) - testutil.AssertEqual(t, 0.0425, values[6].Value) + assert.Len(values, 7) + assert.Equal(0.2127, values[0].Value) + assert.Equal(0.0425, values[6].Value) } diff --git a/vector_renderer.go b/vector_renderer.go index 979bea7..e418efe 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -7,10 +7,10 @@ import ( "math" "strings" - "golang.org/x/image/font" - - "git.smarteching.com/zeni/go-chart/v2/drawing" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" + "github.com/wcharczuk/go-chart/util" + "golang.org/x/image/font" ) // SVG returns a new png/raster renderer. @@ -27,25 +27,6 @@ func SVG(width, height int) (Renderer, error) { }, nil } -// SVGWithCSS returns a new png/raster renderer with attached custom CSS -// The optional nonce argument sets a CSP nonce. -func SVGWithCSS(css string, nonce string) func(width, height int) (Renderer, error) { - return func(width, height int) (Renderer, error) { - buffer := bytes.NewBuffer([]byte{}) - canvas := newCanvas(buffer) - canvas.css = css - canvas.nonce = nonce - canvas.Start(width, height) - return &vectorRenderer{ - b: buffer, - c: canvas, - s: &Style{}, - p: []string{}, - dpi: DefaultDPI, - }, nil - } -} - // vectorRenderer renders chart commands to a bitmap. type vectorRenderer struct { dpi float64 @@ -72,11 +53,6 @@ func (vr *vectorRenderer) SetDPI(dpi float64) { vr.c.dpi = dpi } -// SetClassName implements the interface method. -func (vr *vectorRenderer) SetClassName(classname string) { - vr.s.ClassName = classname -} - // SetStrokeColor implements the interface method. func (vr *vectorRenderer) SetStrokeColor(c drawing.Color) { vr.s.StrokeColor = c @@ -113,8 +89,8 @@ func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) { } func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) { - startAngle = RadianAdd(startAngle, _pi2) - endAngle := RadianAdd(startAngle, delta) + startAngle = util.Math.RadianAdd(startAngle, _pi2) + endAngle := util.Math.RadianAdd(startAngle, delta) startx := cx + int(rx*math.Sin(startAngle)) starty := cy - int(ry*math.Cos(startAngle)) @@ -128,19 +104,14 @@ func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) { endx := cx + int(rx*math.Sin(endAngle)) endy := cy - int(ry*math.Cos(endAngle)) - dd := RadiansToDegrees(delta) + dd := util.Math.RadiansToDegrees(delta) - largeArcFlag := 0 - if delta > _pi { - largeArcFlag = 1 - } - - vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f %d 1 %d %d", int(rx), int(ry), dd, largeArcFlag, endx, endy)) + vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f 0 1 %d %d", int(rx), int(ry), dd, endx, endy)) } // Close closes a shape. func (vr *vectorRenderer) Close() { - vr.p = append(vr.p, "Z") + vr.p = append(vr.p, fmt.Sprintf("Z")) } // Stroke draws the path with no fill. @@ -190,7 +161,8 @@ func (vr *vectorRenderer) Text(body string, x, y int) { } // MeasureText uses the truetype font drawer to measure the width of text. -func (vr *vectorRenderer) MeasureText(body string) (box Box) { +func (vr *vectorRenderer) MeasureText(body string) Box2d { + var box Box if vr.s.GetFont() != nil { vr.fc = &font.Drawer{ Face: truetype.NewFace(vr.s.GetFont(), &truetype.Options{ @@ -203,11 +175,11 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) { box.Right = w box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize)) if vr.c.textTheta == nil { - return + return box.Corners() } - box = box.Corners().Rotate(RadiansToDegrees(*vr.c.textTheta)).Box() + return box.Corners().Rotate(util.Math.RadiansToDegrees(*vr.c.textTheta)) } - return + return box.Corners() } // SetTextRotation sets the text rotation. @@ -240,23 +212,12 @@ type canvas struct { textTheta *float64 width int height int - css string - nonce string } func (c *canvas) Start(width, height int) { c.width = width c.height = height - c.w.Write([]byte(fmt.Sprintf(``, c.width, c.height))) - if c.css != "" { - c.w.Write([]byte(``, c.css))) - } + c.w.Write([]byte(fmt.Sprintf(`\n`, c.width, c.height))) } func (c *canvas) Path(d string, style Style) { @@ -264,20 +225,20 @@ func (c *canvas) Path(d string, style Style) { if len(style.StrokeDashArray) > 0 { strokeDashArrayProperty = c.getStrokeDashArray(style) } - c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style)))) + c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style)))) } func (c *canvas) Text(x, y int, body string, style Style) { if c.textTheta == nil { - c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) } else { - transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, RadiansToDegrees(*c.textTheta), x, y) - c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body))) + transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, util.Math.RadiansToDegrees(*c.textTheta), x, y) + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body))) } } func (c *canvas) Circle(x, y, r int, style Style) { - c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style)))) + c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style)))) } func (c *canvas) End() { @@ -308,7 +269,7 @@ func (c *canvas) getFontFace(s Style) string { return fmt.Sprintf("font-family:%s", family) } -// styleAsSVG returns the style as a svg style or class string. +// styleAsSVG returns the style as a svg style string. func (c *canvas) styleAsSVG(s Style) string { sw := s.StrokeWidth sc := s.StrokeColor @@ -316,22 +277,6 @@ func (c *canvas) styleAsSVG(s Style) string { fs := s.FontSize fnc := s.FontColor - if s.ClassName != "" { - var classes []string - classes = append(classes, s.ClassName) - if !sc.IsZero() { - classes = append(classes, "stroke") - } - if !fc.IsZero() { - classes = append(classes, "fill") - } - if fs != 0 || s.Font != nil { - classes = append(classes, "text") - } - - return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " ")) - } - var pieces []string if sw != 0 { @@ -361,5 +306,5 @@ func (c *canvas) styleAsSVG(s Style) string { if s.Font != nil { pieces = append(pieces, c.getFontFace(s)) } - return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";")) + return strings.Join(pieces, ";") } diff --git a/vector_renderer_test.go b/vector_renderer_test.go index e9556d1..f802970 100644 --- a/vector_renderer_test.go +++ b/vector_renderer_test.go @@ -2,22 +2,21 @@ package chart import ( "bytes" - "fmt" "strings" "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 TestVectorRendererPath(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) vr, err := SVG(100, 100) - testutil.AssertNil(t, err) + assert.Nil(err) typed, isTyped := vr.(*vectorRenderer) - testutil.AssertTrue(t, isTyped) + assert.True(isTyped) typed.MoveTo(0, 0) typed.LineTo(100, 100) @@ -27,37 +26,37 @@ func TestVectorRendererPath(t *testing.T) { buffer := bytes.NewBuffer([]byte{}) err = typed.Save(buffer) - testutil.AssertNil(t, err) + assert.Nil(err) raw := string(buffer.Bytes()) - testutil.AssertTrue(t, strings.HasPrefix(raw, "")) + assert.True(strings.HasPrefix(raw, "")) } func TestVectorRendererMeasureText(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) vr, err := SVG(100, 100) - testutil.AssertNil(t, err) + assert.Nil(err) vr.SetDPI(DefaultDPI) vr.SetFont(f) vr.SetFontSize(12.0) tb := vr.MeasureText("Ljp") - testutil.AssertEqual(t, 21, tb.Width()) - testutil.AssertEqual(t, 15, tb.Height()) + assert.Equal(21, tb.Width()) + assert.Equal(15, tb.Height()) } func TestCanvasStyleSVG(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) set := Style{ StrokeColor: drawing.ColorWhite, @@ -71,47 +70,8 @@ func TestCanvasStyleSVG(t *testing.T) { canvas := &canvas{dpi: DefaultDPI} svgString := canvas.styleAsSVG(set) - testutil.AssertNotEmpty(t, svgString) - testutil.AssertTrue(t, strings.HasPrefix(svgString, "style=\"")) - testutil.AssertTrue(t, strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)")) - testutil.AssertTrue(t, strings.Contains(svgString, "stroke-width:5")) - testutil.AssertTrue(t, strings.Contains(svgString, "fill:rgba(255,255,255,1.0)")) - testutil.AssertTrue(t, strings.HasSuffix(svgString, "\"")) -} - -func TestCanvasClassSVG(t *testing.T) { - set := Style{ - ClassName: "test-class", - } - - canvas := &canvas{dpi: DefaultDPI} - - testutil.AssertEqual(t, "class=\"test-class\"", canvas.styleAsSVG(set)) -} - -func TestCanvasCustomInlineStylesheet(t *testing.T) { - b := strings.Builder{} - - canvas := &canvas{ - w: &b, - css: ".background { fill: red }", - } - - canvas.Start(200, 200) - - testutil.AssertContains(t, b.String(), fmt.Sprintf(``, canvas.css)) -} - -func TestCanvasCustomInlineStylesheetWithNonce(t *testing.T) { - b := strings.Builder{} - - canvas := &canvas{ - w: &b, - css: ".background { fill: red }", - nonce: "RAND0MSTRING", - } - - canvas.Start(200, 200) - - testutil.AssertContains(t, b.String(), fmt.Sprintf(``, canvas.nonce, canvas.css)) + assert.NotEmpty(svgString) + assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)")) + assert.True(strings.Contains(svgString, "stroke-width:5")) + assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)")) } diff --git a/viridis.go b/viridis.go index 81f42a6..e801537 100644 --- a/viridis.go +++ b/viridis.go @@ -1,264 +1,264 @@ package chart -import "git.smarteching.com/zeni/go-chart/v2/drawing" +import "github.com/wcharczuk/go-chart/drawing" var viridisColors = [256]drawing.Color{ - {R: 0x44, G: 0x1, B: 0x54, A: 0xff}, - {R: 0x44, G: 0x2, B: 0x55, A: 0xff}, - {R: 0x45, G: 0x3, B: 0x57, A: 0xff}, - {R: 0x45, G: 0x5, B: 0x58, A: 0xff}, - {R: 0x45, G: 0x6, B: 0x5a, A: 0xff}, - {R: 0x46, G: 0x8, B: 0x5b, A: 0xff}, - {R: 0x46, G: 0x9, B: 0x5d, A: 0xff}, - {R: 0x46, G: 0xb, B: 0x5e, A: 0xff}, - {R: 0x46, G: 0xc, B: 0x60, A: 0xff}, - {R: 0x47, G: 0xe, B: 0x61, A: 0xff}, - {R: 0x47, G: 0xf, B: 0x62, A: 0xff}, - {R: 0x47, G: 0x11, B: 0x64, A: 0xff}, - {R: 0x47, G: 0x12, B: 0x65, A: 0xff}, - {R: 0x47, G: 0x14, B: 0x66, A: 0xff}, - {R: 0x48, G: 0x15, B: 0x68, A: 0xff}, - {R: 0x48, G: 0x16, B: 0x69, A: 0xff}, - {R: 0x48, G: 0x18, B: 0x6a, A: 0xff}, - {R: 0x48, G: 0x19, B: 0x6c, A: 0xff}, - {R: 0x48, G: 0x1a, B: 0x6d, A: 0xff}, - {R: 0x48, G: 0x1c, B: 0x6e, A: 0xff}, - {R: 0x48, G: 0x1d, B: 0x6f, A: 0xff}, - {R: 0x48, G: 0x1e, B: 0x70, A: 0xff}, - {R: 0x48, G: 0x20, B: 0x71, A: 0xff}, - {R: 0x48, G: 0x21, B: 0x73, A: 0xff}, - {R: 0x48, G: 0x22, B: 0x74, A: 0xff}, - {R: 0x48, G: 0x24, B: 0x75, A: 0xff}, - {R: 0x48, G: 0x25, B: 0x76, A: 0xff}, - {R: 0x48, G: 0x26, B: 0x77, A: 0xff}, - {R: 0x48, G: 0x27, B: 0x78, A: 0xff}, - {R: 0x47, G: 0x29, B: 0x79, A: 0xff}, - {R: 0x47, G: 0x2a, B: 0x79, A: 0xff}, - {R: 0x47, G: 0x2b, B: 0x7a, A: 0xff}, - {R: 0x47, G: 0x2c, B: 0x7b, A: 0xff}, - {R: 0x47, G: 0x2e, B: 0x7c, A: 0xff}, - {R: 0x46, G: 0x2f, B: 0x7d, A: 0xff}, - {R: 0x46, G: 0x30, B: 0x7e, A: 0xff}, - {R: 0x46, G: 0x31, B: 0x7e, A: 0xff}, - {R: 0x46, G: 0x33, B: 0x7f, A: 0xff}, - {R: 0x45, G: 0x34, B: 0x80, A: 0xff}, - {R: 0x45, G: 0x35, B: 0x81, A: 0xff}, - {R: 0x45, G: 0x36, B: 0x81, A: 0xff}, - {R: 0x44, G: 0x38, B: 0x82, A: 0xff}, - {R: 0x44, G: 0x39, B: 0x83, A: 0xff}, - {R: 0x44, G: 0x3a, B: 0x83, A: 0xff}, - {R: 0x43, G: 0x3b, B: 0x84, A: 0xff}, - {R: 0x43, G: 0x3c, B: 0x84, A: 0xff}, - {R: 0x43, G: 0x3e, B: 0x85, A: 0xff}, - {R: 0x42, G: 0x3f, B: 0x85, A: 0xff}, - {R: 0x42, G: 0x40, B: 0x86, A: 0xff}, - {R: 0x41, G: 0x41, B: 0x86, A: 0xff}, - {R: 0x41, G: 0x42, B: 0x87, A: 0xff}, - {R: 0x41, G: 0x43, B: 0x87, A: 0xff}, - {R: 0x40, G: 0x45, B: 0x88, A: 0xff}, - {R: 0x40, G: 0x46, B: 0x88, A: 0xff}, - {R: 0x3f, G: 0x47, B: 0x88, A: 0xff}, - {R: 0x3f, G: 0x48, B: 0x89, A: 0xff}, - {R: 0x3e, G: 0x49, B: 0x89, A: 0xff}, - {R: 0x3e, G: 0x4a, B: 0x89, A: 0xff}, - {R: 0x3d, G: 0x4b, B: 0x8a, A: 0xff}, - {R: 0x3d, G: 0x4d, B: 0x8a, A: 0xff}, - {R: 0x3c, G: 0x4e, B: 0x8a, A: 0xff}, - {R: 0x3c, G: 0x4f, B: 0x8a, A: 0xff}, - {R: 0x3b, G: 0x50, B: 0x8b, A: 0xff}, - {R: 0x3b, G: 0x51, B: 0x8b, A: 0xff}, - {R: 0x3a, G: 0x52, B: 0x8b, A: 0xff}, - {R: 0x3a, G: 0x53, B: 0x8b, A: 0xff}, - {R: 0x39, G: 0x54, B: 0x8c, A: 0xff}, - {R: 0x39, G: 0x55, B: 0x8c, A: 0xff}, - {R: 0x38, G: 0x56, B: 0x8c, A: 0xff}, - {R: 0x38, G: 0x57, B: 0x8c, A: 0xff}, - {R: 0x37, G: 0x58, B: 0x8c, A: 0xff}, - {R: 0x37, G: 0x59, B: 0x8c, A: 0xff}, - {R: 0x36, G: 0x5b, B: 0x8d, A: 0xff}, - {R: 0x36, G: 0x5c, B: 0x8d, A: 0xff}, - {R: 0x35, G: 0x5d, B: 0x8d, A: 0xff}, - {R: 0x35, G: 0x5e, B: 0x8d, A: 0xff}, - {R: 0x34, G: 0x5f, B: 0x8d, A: 0xff}, - {R: 0x34, G: 0x60, B: 0x8d, A: 0xff}, - {R: 0x33, G: 0x61, B: 0x8d, A: 0xff}, - {R: 0x33, G: 0x62, B: 0x8d, A: 0xff}, - {R: 0x33, G: 0x63, B: 0x8d, A: 0xff}, - {R: 0x32, G: 0x64, B: 0x8e, A: 0xff}, - {R: 0x32, G: 0x65, B: 0x8e, A: 0xff}, - {R: 0x31, G: 0x66, B: 0x8e, A: 0xff}, - {R: 0x31, G: 0x67, B: 0x8e, A: 0xff}, - {R: 0x30, G: 0x68, B: 0x8e, A: 0xff}, - {R: 0x30, G: 0x69, B: 0x8e, A: 0xff}, - {R: 0x2f, G: 0x6a, B: 0x8e, A: 0xff}, - {R: 0x2f, G: 0x6b, B: 0x8e, A: 0xff}, - {R: 0x2f, G: 0x6c, B: 0x8e, A: 0xff}, - {R: 0x2e, G: 0x6d, B: 0x8e, A: 0xff}, - {R: 0x2e, G: 0x6e, B: 0x8e, A: 0xff}, - {R: 0x2d, G: 0x6f, B: 0x8e, A: 0xff}, - {R: 0x2d, G: 0x70, B: 0x8e, A: 0xff}, - {R: 0x2d, G: 0x70, B: 0x8e, A: 0xff}, - {R: 0x2c, G: 0x71, B: 0x8e, A: 0xff}, - {R: 0x2c, G: 0x72, B: 0x8e, A: 0xff}, - {R: 0x2b, G: 0x73, B: 0x8e, A: 0xff}, - {R: 0x2b, G: 0x74, B: 0x8e, A: 0xff}, - {R: 0x2b, G: 0x75, B: 0x8e, A: 0xff}, - {R: 0x2a, G: 0x76, B: 0x8e, A: 0xff}, - {R: 0x2a, G: 0x77, B: 0x8e, A: 0xff}, - {R: 0x29, G: 0x78, B: 0x8e, A: 0xff}, - {R: 0x29, G: 0x79, B: 0x8e, A: 0xff}, - {R: 0x29, G: 0x7a, B: 0x8e, A: 0xff}, - {R: 0x28, G: 0x7b, B: 0x8e, A: 0xff}, - {R: 0x28, G: 0x7c, B: 0x8e, A: 0xff}, - {R: 0x28, G: 0x7d, B: 0x8e, A: 0xff}, - {R: 0x27, G: 0x7e, B: 0x8e, A: 0xff}, - {R: 0x27, G: 0x7f, B: 0x8e, A: 0xff}, - {R: 0x26, G: 0x80, B: 0x8e, A: 0xff}, - {R: 0x26, G: 0x81, B: 0x8e, A: 0xff}, - {R: 0x26, G: 0x82, B: 0x8e, A: 0xff}, - {R: 0x25, G: 0x83, B: 0x8e, A: 0xff}, - {R: 0x25, G: 0x83, B: 0x8e, A: 0xff}, - {R: 0x25, G: 0x84, B: 0x8e, A: 0xff}, - {R: 0x24, G: 0x85, B: 0x8e, A: 0xff}, - {R: 0x24, G: 0x86, B: 0x8e, A: 0xff}, - {R: 0x23, G: 0x87, B: 0x8e, A: 0xff}, - {R: 0x23, G: 0x88, B: 0x8e, A: 0xff}, - {R: 0x23, G: 0x89, B: 0x8e, A: 0xff}, - {R: 0x22, G: 0x8a, B: 0x8d, A: 0xff}, - {R: 0x22, G: 0x8b, B: 0x8d, A: 0xff}, - {R: 0x22, G: 0x8c, B: 0x8d, A: 0xff}, - {R: 0x21, G: 0x8d, B: 0x8d, A: 0xff}, - {R: 0x21, G: 0x8e, B: 0x8d, A: 0xff}, - {R: 0x21, G: 0x8f, B: 0x8d, A: 0xff}, - {R: 0x20, G: 0x90, B: 0x8d, A: 0xff}, - {R: 0x20, G: 0x91, B: 0x8c, A: 0xff}, - {R: 0x20, G: 0x92, B: 0x8c, A: 0xff}, - {R: 0x20, G: 0x93, B: 0x8c, A: 0xff}, - {R: 0x1f, G: 0x93, B: 0x8c, A: 0xff}, - {R: 0x1f, G: 0x94, B: 0x8c, A: 0xff}, - {R: 0x1f, G: 0x95, B: 0x8b, A: 0xff}, - {R: 0x1f, G: 0x96, B: 0x8b, A: 0xff}, - {R: 0x1f, G: 0x97, B: 0x8b, A: 0xff}, - {R: 0x1e, G: 0x98, B: 0x8b, A: 0xff}, - {R: 0x1e, G: 0x99, B: 0x8a, A: 0xff}, - {R: 0x1e, G: 0x9a, B: 0x8a, A: 0xff}, - {R: 0x1e, G: 0x9b, B: 0x8a, A: 0xff}, - {R: 0x1e, G: 0x9c, B: 0x89, A: 0xff}, - {R: 0x1e, G: 0x9d, B: 0x89, A: 0xff}, - {R: 0x1e, G: 0x9e, B: 0x89, A: 0xff}, - {R: 0x1e, G: 0x9f, B: 0x88, A: 0xff}, - {R: 0x1e, G: 0xa0, B: 0x88, A: 0xff}, - {R: 0x1f, G: 0xa1, B: 0x88, A: 0xff}, - {R: 0x1f, G: 0xa2, B: 0x87, A: 0xff}, - {R: 0x1f, G: 0xa3, B: 0x87, A: 0xff}, - {R: 0x1f, G: 0xa3, B: 0x86, A: 0xff}, - {R: 0x20, G: 0xa4, B: 0x86, A: 0xff}, - {R: 0x20, G: 0xa5, B: 0x86, A: 0xff}, - {R: 0x21, G: 0xa6, B: 0x85, A: 0xff}, - {R: 0x21, G: 0xa7, B: 0x85, A: 0xff}, - {R: 0x22, G: 0xa8, B: 0x84, A: 0xff}, - {R: 0x23, G: 0xa9, B: 0x83, A: 0xff}, - {R: 0x23, G: 0xaa, B: 0x83, A: 0xff}, - {R: 0x24, G: 0xab, B: 0x82, A: 0xff}, - {R: 0x25, G: 0xac, B: 0x82, A: 0xff}, - {R: 0x26, G: 0xad, B: 0x81, A: 0xff}, - {R: 0x27, G: 0xae, B: 0x81, A: 0xff}, - {R: 0x28, G: 0xaf, B: 0x80, A: 0xff}, - {R: 0x29, G: 0xaf, B: 0x7f, A: 0xff}, - {R: 0x2a, G: 0xb0, B: 0x7f, A: 0xff}, - {R: 0x2b, G: 0xb1, B: 0x7e, A: 0xff}, - {R: 0x2c, G: 0xb2, B: 0x7d, A: 0xff}, - {R: 0x2e, G: 0xb3, B: 0x7c, A: 0xff}, - {R: 0x2f, G: 0xb4, B: 0x7c, A: 0xff}, - {R: 0x30, G: 0xb5, B: 0x7b, A: 0xff}, - {R: 0x32, G: 0xb6, B: 0x7a, A: 0xff}, - {R: 0x33, G: 0xb7, B: 0x79, A: 0xff}, - {R: 0x35, G: 0xb7, B: 0x79, A: 0xff}, - {R: 0x36, G: 0xb8, B: 0x78, A: 0xff}, - {R: 0x38, G: 0xb9, B: 0x77, A: 0xff}, - {R: 0x39, G: 0xba, B: 0x76, A: 0xff}, - {R: 0x3b, G: 0xbb, B: 0x75, A: 0xff}, - {R: 0x3d, G: 0xbc, B: 0x74, A: 0xff}, - {R: 0x3e, G: 0xbd, B: 0x73, A: 0xff}, - {R: 0x40, G: 0xbe, B: 0x72, A: 0xff}, - {R: 0x42, G: 0xbe, B: 0x71, A: 0xff}, - {R: 0x44, G: 0xbf, B: 0x70, A: 0xff}, - {R: 0x46, G: 0xc0, B: 0x6f, A: 0xff}, - {R: 0x48, G: 0xc1, B: 0x6e, A: 0xff}, - {R: 0x49, G: 0xc2, B: 0x6d, A: 0xff}, - {R: 0x4b, G: 0xc2, B: 0x6c, A: 0xff}, - {R: 0x4d, G: 0xc3, B: 0x6b, A: 0xff}, - {R: 0x4f, G: 0xc4, B: 0x6a, A: 0xff}, - {R: 0x51, G: 0xc5, B: 0x69, A: 0xff}, - {R: 0x53, G: 0xc6, B: 0x68, A: 0xff}, - {R: 0x55, G: 0xc6, B: 0x66, A: 0xff}, - {R: 0x58, G: 0xc7, B: 0x65, A: 0xff}, - {R: 0x5a, G: 0xc8, B: 0x64, A: 0xff}, - {R: 0x5c, G: 0xc9, B: 0x63, A: 0xff}, - {R: 0x5e, G: 0xc9, B: 0x62, A: 0xff}, - {R: 0x60, G: 0xca, B: 0x60, A: 0xff}, - {R: 0x62, G: 0xcb, B: 0x5f, A: 0xff}, - {R: 0x65, G: 0xcc, B: 0x5e, A: 0xff}, - {R: 0x67, G: 0xcc, B: 0x5c, A: 0xff}, - {R: 0x69, G: 0xcd, B: 0x5b, A: 0xff}, - {R: 0x6c, G: 0xce, B: 0x5a, A: 0xff}, - {R: 0x6e, G: 0xce, B: 0x58, A: 0xff}, - {R: 0x70, G: 0xcf, B: 0x57, A: 0xff}, - {R: 0x73, G: 0xd0, B: 0x55, A: 0xff}, - {R: 0x75, G: 0xd0, B: 0x54, A: 0xff}, - {R: 0x77, G: 0xd1, B: 0x52, A: 0xff}, - {R: 0x7a, G: 0xd2, B: 0x51, A: 0xff}, - {R: 0x7c, G: 0xd2, B: 0x4f, A: 0xff}, - {R: 0x7f, G: 0xd3, B: 0x4e, A: 0xff}, - {R: 0x81, G: 0xd4, B: 0x4c, A: 0xff}, - {R: 0x84, G: 0xd4, B: 0x4b, A: 0xff}, - {R: 0x86, G: 0xd5, B: 0x49, A: 0xff}, - {R: 0x89, G: 0xd5, B: 0x48, A: 0xff}, - {R: 0x8b, G: 0xd6, B: 0x46, A: 0xff}, - {R: 0x8e, G: 0xd7, B: 0x44, A: 0xff}, - {R: 0x90, G: 0xd7, B: 0x43, A: 0xff}, - {R: 0x93, G: 0xd8, B: 0x41, A: 0xff}, - {R: 0x95, G: 0xd8, B: 0x3f, A: 0xff}, - {R: 0x98, G: 0xd9, B: 0x3e, A: 0xff}, - {R: 0x9b, G: 0xd9, B: 0x3c, A: 0xff}, - {R: 0x9d, G: 0xda, B: 0x3a, A: 0xff}, - {R: 0xa0, G: 0xda, B: 0x39, A: 0xff}, - {R: 0xa3, G: 0xdb, B: 0x37, A: 0xff}, - {R: 0xa5, G: 0xdb, B: 0x35, A: 0xff}, - {R: 0xa8, G: 0xdc, B: 0x33, A: 0xff}, - {R: 0xab, G: 0xdc, B: 0x32, A: 0xff}, - {R: 0xad, G: 0xdd, B: 0x30, A: 0xff}, - {R: 0xb0, G: 0xdd, B: 0x2e, A: 0xff}, - {R: 0xb3, G: 0xdd, B: 0x2d, A: 0xff}, - {R: 0xb5, G: 0xde, B: 0x2b, A: 0xff}, - {R: 0xb8, G: 0xde, B: 0x29, A: 0xff}, - {R: 0xbb, G: 0xdf, B: 0x27, A: 0xff}, - {R: 0xbd, G: 0xdf, B: 0x26, A: 0xff}, - {R: 0xc0, G: 0xdf, B: 0x24, A: 0xff}, - {R: 0xc3, G: 0xe0, B: 0x23, A: 0xff}, - {R: 0xc5, G: 0xe0, B: 0x21, A: 0xff}, - {R: 0xc8, G: 0xe1, B: 0x20, A: 0xff}, - {R: 0xcb, G: 0xe1, B: 0x1e, A: 0xff}, - {R: 0xcd, G: 0xe1, B: 0x1d, A: 0xff}, - {R: 0xd0, G: 0xe2, B: 0x1c, A: 0xff}, - {R: 0xd3, G: 0xe2, B: 0x1b, A: 0xff}, - {R: 0xd5, G: 0xe2, B: 0x1a, A: 0xff}, - {R: 0xd8, G: 0xe3, B: 0x19, A: 0xff}, - {R: 0xdb, G: 0xe3, B: 0x18, A: 0xff}, - {R: 0xdd, G: 0xe3, B: 0x18, A: 0xff}, - {R: 0xe0, G: 0xe4, B: 0x18, A: 0xff}, - {R: 0xe2, G: 0xe4, B: 0x18, A: 0xff}, - {R: 0xe5, G: 0xe4, B: 0x18, A: 0xff}, - {R: 0xe8, G: 0xe5, B: 0x19, A: 0xff}, - {R: 0xea, G: 0xe5, B: 0x19, A: 0xff}, - {R: 0xed, G: 0xe5, B: 0x1a, A: 0xff}, - {R: 0xef, G: 0xe6, B: 0x1b, A: 0xff}, - {R: 0xf2, G: 0xe6, B: 0x1c, A: 0xff}, - {R: 0xf4, G: 0xe6, B: 0x1e, A: 0xff}, - {R: 0xf7, G: 0xe6, B: 0x1f, A: 0xff}, - {R: 0xf9, G: 0xe7, B: 0x21, A: 0xff}, - {R: 0xfb, G: 0xe7, B: 0x23, A: 0xff}, - {R: 0xfe, G: 0xe7, B: 0x24, A: 0xff}, + drawing.Color{R: 0x44, G: 0x1, B: 0x54, A: 0xff}, + drawing.Color{R: 0x44, G: 0x2, B: 0x55, A: 0xff}, + drawing.Color{R: 0x45, G: 0x3, B: 0x57, A: 0xff}, + drawing.Color{R: 0x45, G: 0x5, B: 0x58, A: 0xff}, + drawing.Color{R: 0x45, G: 0x6, B: 0x5a, A: 0xff}, + drawing.Color{R: 0x46, G: 0x8, B: 0x5b, A: 0xff}, + drawing.Color{R: 0x46, G: 0x9, B: 0x5d, A: 0xff}, + drawing.Color{R: 0x46, G: 0xb, B: 0x5e, A: 0xff}, + drawing.Color{R: 0x46, G: 0xc, B: 0x60, A: 0xff}, + drawing.Color{R: 0x47, G: 0xe, B: 0x61, A: 0xff}, + drawing.Color{R: 0x47, G: 0xf, B: 0x62, A: 0xff}, + drawing.Color{R: 0x47, G: 0x11, B: 0x64, A: 0xff}, + drawing.Color{R: 0x47, G: 0x12, B: 0x65, A: 0xff}, + drawing.Color{R: 0x47, G: 0x14, B: 0x66, A: 0xff}, + drawing.Color{R: 0x48, G: 0x15, B: 0x68, A: 0xff}, + drawing.Color{R: 0x48, G: 0x16, B: 0x69, A: 0xff}, + drawing.Color{R: 0x48, G: 0x18, B: 0x6a, A: 0xff}, + drawing.Color{R: 0x48, G: 0x19, B: 0x6c, A: 0xff}, + drawing.Color{R: 0x48, G: 0x1a, B: 0x6d, A: 0xff}, + drawing.Color{R: 0x48, G: 0x1c, B: 0x6e, A: 0xff}, + drawing.Color{R: 0x48, G: 0x1d, B: 0x6f, A: 0xff}, + drawing.Color{R: 0x48, G: 0x1e, B: 0x70, A: 0xff}, + drawing.Color{R: 0x48, G: 0x20, B: 0x71, A: 0xff}, + drawing.Color{R: 0x48, G: 0x21, B: 0x73, A: 0xff}, + drawing.Color{R: 0x48, G: 0x22, B: 0x74, A: 0xff}, + drawing.Color{R: 0x48, G: 0x24, B: 0x75, A: 0xff}, + drawing.Color{R: 0x48, G: 0x25, B: 0x76, A: 0xff}, + drawing.Color{R: 0x48, G: 0x26, B: 0x77, A: 0xff}, + drawing.Color{R: 0x48, G: 0x27, B: 0x78, A: 0xff}, + drawing.Color{R: 0x47, G: 0x29, B: 0x79, A: 0xff}, + drawing.Color{R: 0x47, G: 0x2a, B: 0x79, A: 0xff}, + drawing.Color{R: 0x47, G: 0x2b, B: 0x7a, A: 0xff}, + drawing.Color{R: 0x47, G: 0x2c, B: 0x7b, A: 0xff}, + drawing.Color{R: 0x47, G: 0x2e, B: 0x7c, A: 0xff}, + drawing.Color{R: 0x46, G: 0x2f, B: 0x7d, A: 0xff}, + drawing.Color{R: 0x46, G: 0x30, B: 0x7e, A: 0xff}, + drawing.Color{R: 0x46, G: 0x31, B: 0x7e, A: 0xff}, + drawing.Color{R: 0x46, G: 0x33, B: 0x7f, A: 0xff}, + drawing.Color{R: 0x45, G: 0x34, B: 0x80, A: 0xff}, + drawing.Color{R: 0x45, G: 0x35, B: 0x81, A: 0xff}, + drawing.Color{R: 0x45, G: 0x36, B: 0x81, A: 0xff}, + drawing.Color{R: 0x44, G: 0x38, B: 0x82, A: 0xff}, + drawing.Color{R: 0x44, G: 0x39, B: 0x83, A: 0xff}, + drawing.Color{R: 0x44, G: 0x3a, B: 0x83, A: 0xff}, + drawing.Color{R: 0x43, G: 0x3b, B: 0x84, A: 0xff}, + drawing.Color{R: 0x43, G: 0x3c, B: 0x84, A: 0xff}, + drawing.Color{R: 0x43, G: 0x3e, B: 0x85, A: 0xff}, + drawing.Color{R: 0x42, G: 0x3f, B: 0x85, A: 0xff}, + drawing.Color{R: 0x42, G: 0x40, B: 0x86, A: 0xff}, + drawing.Color{R: 0x41, G: 0x41, B: 0x86, A: 0xff}, + drawing.Color{R: 0x41, G: 0x42, B: 0x87, A: 0xff}, + drawing.Color{R: 0x41, G: 0x43, B: 0x87, A: 0xff}, + drawing.Color{R: 0x40, G: 0x45, B: 0x88, A: 0xff}, + drawing.Color{R: 0x40, G: 0x46, B: 0x88, A: 0xff}, + drawing.Color{R: 0x3f, G: 0x47, B: 0x88, A: 0xff}, + drawing.Color{R: 0x3f, G: 0x48, B: 0x89, A: 0xff}, + drawing.Color{R: 0x3e, G: 0x49, B: 0x89, A: 0xff}, + drawing.Color{R: 0x3e, G: 0x4a, B: 0x89, A: 0xff}, + drawing.Color{R: 0x3d, G: 0x4b, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x3d, G: 0x4d, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x3c, G: 0x4e, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x3c, G: 0x4f, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x3b, G: 0x50, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x3b, G: 0x51, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x3a, G: 0x52, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x3a, G: 0x53, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x39, G: 0x54, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x39, G: 0x55, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x38, G: 0x56, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x38, G: 0x57, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x37, G: 0x58, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x37, G: 0x59, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x36, G: 0x5b, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x36, G: 0x5c, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x35, G: 0x5d, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x35, G: 0x5e, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x34, G: 0x5f, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x34, G: 0x60, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x33, G: 0x61, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x33, G: 0x62, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x33, G: 0x63, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x32, G: 0x64, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x32, G: 0x65, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x31, G: 0x66, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x31, G: 0x67, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x30, G: 0x68, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x30, G: 0x69, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2f, G: 0x6a, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2f, G: 0x6b, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2f, G: 0x6c, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2e, G: 0x6d, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2e, G: 0x6e, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2d, G: 0x6f, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2d, G: 0x70, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2d, G: 0x70, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2c, G: 0x71, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2c, G: 0x72, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2b, G: 0x73, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2b, G: 0x74, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2b, G: 0x75, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2a, G: 0x76, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x2a, G: 0x77, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x29, G: 0x78, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x29, G: 0x79, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x29, G: 0x7a, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x28, G: 0x7b, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x28, G: 0x7c, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x28, G: 0x7d, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x27, G: 0x7e, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x27, G: 0x7f, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x26, G: 0x80, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x26, G: 0x81, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x26, G: 0x82, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x25, G: 0x83, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x25, G: 0x83, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x25, G: 0x84, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x24, G: 0x85, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x24, G: 0x86, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x23, G: 0x87, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x23, G: 0x88, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x23, G: 0x89, B: 0x8e, A: 0xff}, + drawing.Color{R: 0x22, G: 0x8a, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x22, G: 0x8b, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x22, G: 0x8c, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x21, G: 0x8d, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x21, G: 0x8e, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x21, G: 0x8f, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x20, G: 0x90, B: 0x8d, A: 0xff}, + drawing.Color{R: 0x20, G: 0x91, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x20, G: 0x92, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x20, G: 0x93, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x1f, G: 0x93, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x1f, G: 0x94, B: 0x8c, A: 0xff}, + drawing.Color{R: 0x1f, G: 0x95, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x1f, G: 0x96, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x1f, G: 0x97, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x98, B: 0x8b, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x99, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9a, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9b, B: 0x8a, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9c, B: 0x89, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9d, B: 0x89, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9e, B: 0x89, A: 0xff}, + drawing.Color{R: 0x1e, G: 0x9f, B: 0x88, A: 0xff}, + drawing.Color{R: 0x1e, G: 0xa0, B: 0x88, A: 0xff}, + drawing.Color{R: 0x1f, G: 0xa1, B: 0x88, A: 0xff}, + drawing.Color{R: 0x1f, G: 0xa2, B: 0x87, A: 0xff}, + drawing.Color{R: 0x1f, G: 0xa3, B: 0x87, A: 0xff}, + drawing.Color{R: 0x1f, G: 0xa3, B: 0x86, A: 0xff}, + drawing.Color{R: 0x20, G: 0xa4, B: 0x86, A: 0xff}, + drawing.Color{R: 0x20, G: 0xa5, B: 0x86, A: 0xff}, + drawing.Color{R: 0x21, G: 0xa6, B: 0x85, A: 0xff}, + drawing.Color{R: 0x21, G: 0xa7, B: 0x85, A: 0xff}, + drawing.Color{R: 0x22, G: 0xa8, B: 0x84, A: 0xff}, + drawing.Color{R: 0x23, G: 0xa9, B: 0x83, A: 0xff}, + drawing.Color{R: 0x23, G: 0xaa, B: 0x83, A: 0xff}, + drawing.Color{R: 0x24, G: 0xab, B: 0x82, A: 0xff}, + drawing.Color{R: 0x25, G: 0xac, B: 0x82, A: 0xff}, + drawing.Color{R: 0x26, G: 0xad, B: 0x81, A: 0xff}, + drawing.Color{R: 0x27, G: 0xae, B: 0x81, A: 0xff}, + drawing.Color{R: 0x28, G: 0xaf, B: 0x80, A: 0xff}, + drawing.Color{R: 0x29, G: 0xaf, B: 0x7f, A: 0xff}, + drawing.Color{R: 0x2a, G: 0xb0, B: 0x7f, A: 0xff}, + drawing.Color{R: 0x2b, G: 0xb1, B: 0x7e, A: 0xff}, + drawing.Color{R: 0x2c, G: 0xb2, B: 0x7d, A: 0xff}, + drawing.Color{R: 0x2e, G: 0xb3, B: 0x7c, A: 0xff}, + drawing.Color{R: 0x2f, G: 0xb4, B: 0x7c, A: 0xff}, + drawing.Color{R: 0x30, G: 0xb5, B: 0x7b, A: 0xff}, + drawing.Color{R: 0x32, G: 0xb6, B: 0x7a, A: 0xff}, + drawing.Color{R: 0x33, G: 0xb7, B: 0x79, A: 0xff}, + drawing.Color{R: 0x35, G: 0xb7, B: 0x79, A: 0xff}, + drawing.Color{R: 0x36, G: 0xb8, B: 0x78, A: 0xff}, + drawing.Color{R: 0x38, G: 0xb9, B: 0x77, A: 0xff}, + drawing.Color{R: 0x39, G: 0xba, B: 0x76, A: 0xff}, + drawing.Color{R: 0x3b, G: 0xbb, B: 0x75, A: 0xff}, + drawing.Color{R: 0x3d, G: 0xbc, B: 0x74, A: 0xff}, + drawing.Color{R: 0x3e, G: 0xbd, B: 0x73, A: 0xff}, + drawing.Color{R: 0x40, G: 0xbe, B: 0x72, A: 0xff}, + drawing.Color{R: 0x42, G: 0xbe, B: 0x71, A: 0xff}, + drawing.Color{R: 0x44, G: 0xbf, B: 0x70, A: 0xff}, + drawing.Color{R: 0x46, G: 0xc0, B: 0x6f, A: 0xff}, + drawing.Color{R: 0x48, G: 0xc1, B: 0x6e, A: 0xff}, + drawing.Color{R: 0x49, G: 0xc2, B: 0x6d, A: 0xff}, + drawing.Color{R: 0x4b, G: 0xc2, B: 0x6c, A: 0xff}, + drawing.Color{R: 0x4d, G: 0xc3, B: 0x6b, A: 0xff}, + drawing.Color{R: 0x4f, G: 0xc4, B: 0x6a, A: 0xff}, + drawing.Color{R: 0x51, G: 0xc5, B: 0x69, A: 0xff}, + drawing.Color{R: 0x53, G: 0xc6, B: 0x68, A: 0xff}, + drawing.Color{R: 0x55, G: 0xc6, B: 0x66, A: 0xff}, + drawing.Color{R: 0x58, G: 0xc7, B: 0x65, A: 0xff}, + drawing.Color{R: 0x5a, G: 0xc8, B: 0x64, A: 0xff}, + drawing.Color{R: 0x5c, G: 0xc9, B: 0x63, A: 0xff}, + drawing.Color{R: 0x5e, G: 0xc9, B: 0x62, A: 0xff}, + drawing.Color{R: 0x60, G: 0xca, B: 0x60, A: 0xff}, + drawing.Color{R: 0x62, G: 0xcb, B: 0x5f, A: 0xff}, + drawing.Color{R: 0x65, G: 0xcc, B: 0x5e, A: 0xff}, + drawing.Color{R: 0x67, G: 0xcc, B: 0x5c, A: 0xff}, + drawing.Color{R: 0x69, G: 0xcd, B: 0x5b, A: 0xff}, + drawing.Color{R: 0x6c, G: 0xce, B: 0x5a, A: 0xff}, + drawing.Color{R: 0x6e, G: 0xce, B: 0x58, A: 0xff}, + drawing.Color{R: 0x70, G: 0xcf, B: 0x57, A: 0xff}, + drawing.Color{R: 0x73, G: 0xd0, B: 0x55, A: 0xff}, + drawing.Color{R: 0x75, G: 0xd0, B: 0x54, A: 0xff}, + drawing.Color{R: 0x77, G: 0xd1, B: 0x52, A: 0xff}, + drawing.Color{R: 0x7a, G: 0xd2, B: 0x51, A: 0xff}, + drawing.Color{R: 0x7c, G: 0xd2, B: 0x4f, A: 0xff}, + drawing.Color{R: 0x7f, G: 0xd3, B: 0x4e, A: 0xff}, + drawing.Color{R: 0x81, G: 0xd4, B: 0x4c, A: 0xff}, + drawing.Color{R: 0x84, G: 0xd4, B: 0x4b, A: 0xff}, + drawing.Color{R: 0x86, G: 0xd5, B: 0x49, A: 0xff}, + drawing.Color{R: 0x89, G: 0xd5, B: 0x48, A: 0xff}, + drawing.Color{R: 0x8b, G: 0xd6, B: 0x46, A: 0xff}, + drawing.Color{R: 0x8e, G: 0xd7, B: 0x44, A: 0xff}, + drawing.Color{R: 0x90, G: 0xd7, B: 0x43, A: 0xff}, + drawing.Color{R: 0x93, G: 0xd8, B: 0x41, A: 0xff}, + drawing.Color{R: 0x95, G: 0xd8, B: 0x3f, A: 0xff}, + drawing.Color{R: 0x98, G: 0xd9, B: 0x3e, A: 0xff}, + drawing.Color{R: 0x9b, G: 0xd9, B: 0x3c, A: 0xff}, + drawing.Color{R: 0x9d, G: 0xda, B: 0x3a, A: 0xff}, + drawing.Color{R: 0xa0, G: 0xda, B: 0x39, A: 0xff}, + drawing.Color{R: 0xa3, G: 0xdb, B: 0x37, A: 0xff}, + drawing.Color{R: 0xa5, G: 0xdb, B: 0x35, A: 0xff}, + drawing.Color{R: 0xa8, G: 0xdc, B: 0x33, A: 0xff}, + drawing.Color{R: 0xab, G: 0xdc, B: 0x32, A: 0xff}, + drawing.Color{R: 0xad, G: 0xdd, B: 0x30, A: 0xff}, + drawing.Color{R: 0xb0, G: 0xdd, B: 0x2e, A: 0xff}, + drawing.Color{R: 0xb3, G: 0xdd, B: 0x2d, A: 0xff}, + drawing.Color{R: 0xb5, G: 0xde, B: 0x2b, A: 0xff}, + drawing.Color{R: 0xb8, G: 0xde, B: 0x29, A: 0xff}, + drawing.Color{R: 0xbb, G: 0xdf, B: 0x27, A: 0xff}, + drawing.Color{R: 0xbd, G: 0xdf, B: 0x26, A: 0xff}, + drawing.Color{R: 0xc0, G: 0xdf, B: 0x24, A: 0xff}, + drawing.Color{R: 0xc3, G: 0xe0, B: 0x23, A: 0xff}, + drawing.Color{R: 0xc5, G: 0xe0, B: 0x21, A: 0xff}, + drawing.Color{R: 0xc8, G: 0xe1, B: 0x20, A: 0xff}, + drawing.Color{R: 0xcb, G: 0xe1, B: 0x1e, A: 0xff}, + drawing.Color{R: 0xcd, G: 0xe1, B: 0x1d, A: 0xff}, + drawing.Color{R: 0xd0, G: 0xe2, B: 0x1c, A: 0xff}, + drawing.Color{R: 0xd3, G: 0xe2, B: 0x1b, A: 0xff}, + drawing.Color{R: 0xd5, G: 0xe2, B: 0x1a, A: 0xff}, + drawing.Color{R: 0xd8, G: 0xe3, B: 0x19, A: 0xff}, + drawing.Color{R: 0xdb, G: 0xe3, B: 0x18, A: 0xff}, + drawing.Color{R: 0xdd, G: 0xe3, B: 0x18, A: 0xff}, + drawing.Color{R: 0xe0, G: 0xe4, B: 0x18, A: 0xff}, + drawing.Color{R: 0xe2, G: 0xe4, B: 0x18, A: 0xff}, + drawing.Color{R: 0xe5, G: 0xe4, B: 0x18, A: 0xff}, + drawing.Color{R: 0xe8, G: 0xe5, B: 0x19, A: 0xff}, + drawing.Color{R: 0xea, G: 0xe5, B: 0x19, A: 0xff}, + drawing.Color{R: 0xed, G: 0xe5, B: 0x1a, A: 0xff}, + drawing.Color{R: 0xef, G: 0xe6, B: 0x1b, A: 0xff}, + drawing.Color{R: 0xf2, G: 0xe6, B: 0x1c, A: 0xff}, + drawing.Color{R: 0xf4, G: 0xe6, B: 0x1e, A: 0xff}, + drawing.Color{R: 0xf7, G: 0xe6, B: 0x1f, A: 0xff}, + drawing.Color{R: 0xf9, G: 0xe7, B: 0x21, A: 0xff}, + drawing.Color{R: 0xfb, G: 0xe7, B: 0x23, A: 0xff}, + drawing.Color{R: 0xfe, G: 0xe7, B: 0x24, A: 0xff}, } // Viridis creates a color map provider. diff --git a/xaxis.go b/xaxis.go index 4fcb569..ac8b2ad 100644 --- a/xaxis.go +++ b/xaxis.go @@ -2,14 +2,9 @@ package chart import ( "math" -) -// HideXAxis hides the x-axis. -func HideXAxis() XAxis { - return XAxis{ - Style: Hidden(), - } -} + util "github.com/wcharczuk/go-chart/util" +) // XAxis represents the horizontal axis. type XAxis struct { @@ -96,11 +91,11 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions()) tx = canvasBox.Left + ra.Translate(v) - ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height() + ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height()) switch tp { case TickPositionUnderTick, TickPositionUnset: - ltx = tx - tb.Width()>>1 - rtx = tx + tb.Width()>>1 + ltx = tx - int(tb.Width())>>1 + rtx = tx + int(tb.Width())>>1 break case TickPositionBetweenTicks: if index > 0 { @@ -110,14 +105,14 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic break } - left = MinInt(left, ltx) - right = MaxInt(right, rtx) - bottom = MaxInt(bottom, ty) + left = util.Math.MinInt(left, ltx) + right = util.Math.MaxInt(right, rtx) + bottom = util.Math.MaxInt(bottom, ty) } - if !xa.NameStyle.Hidden && len(xa.Name) > 0 { + if xa.NameStyle.Show && len(xa.Name) > 0 { tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults)) - bottom += DefaultXAxisMargin + tb.Height() + bottom += DefaultXAxisMargin + int(tb.Height()) } return Box{ @@ -158,13 +153,13 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick switch tp { case TickPositionUnderTick, TickPositionUnset: if tickStyle.TextRotationDegrees == 0 { - tx = tx - tb.Width()>>1 - ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height() + tx = tx - int(tb.Width())>>1 + ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height()) } else { - ty = canvasBox.Bottom + (2 * DefaultXAxisMargin) + ty = canvasBox.Bottom + (1.5 * DefaultXAxisMargin) } Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle) - maxTextHeight = MaxInt(maxTextHeight, tb.Height()) + maxTextHeight = util.Math.MaxInt(maxTextHeight, int(tb.Height())) break case TickPositionBetweenTicks: if index > 0 { @@ -180,23 +175,23 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick }, finalTickStyle) ftb := Text.MeasureLines(r, Text.WrapFit(r, t.Label, tx-ltx, finalTickStyle), finalTickStyle) - maxTextHeight = MaxInt(maxTextHeight, ftb.Height()) + maxTextHeight = util.Math.MaxInt(maxTextHeight, int(ftb.Height())) } break } } nameStyle := xa.NameStyle.InheritFrom(defaults) - if !xa.NameStyle.Hidden && len(xa.Name) > 0 { + if xa.NameStyle.Show && len(xa.Name) > 0 { tb := Draw.MeasureText(r, xa.Name, nameStyle) - tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1) - ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height() + tx := canvasBox.Right - (canvasBox.Width()>>1 + int(tb.Width())>>1) + ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + int(tb.Height()) Draw.Text(r, xa.Name, tx, ty, nameStyle) } - if !xa.GridMajorStyle.Hidden || !xa.GridMinorStyle.Hidden { + if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show { for _, gl := range xa.GetGridLines(ticks) { - if (gl.IsMinor && !xa.GridMinorStyle.Hidden) || (!gl.IsMinor && !xa.GridMajorStyle.Hidden) { + if (gl.IsMinor && xa.GridMinorStyle.Show) || (!gl.IsMinor && xa.GridMajorStyle.Show) { defaults := xa.GridMajorStyle if gl.IsMinor { defaults = xa.GridMinorStyle diff --git a/xaxis_test.go b/xaxis_test.go index 4668851..f55ea29 100644 --- a/xaxis_test.go +++ b/xaxis_test.go @@ -3,17 +3,17 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) func TestXAxisGetTicks(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) xa := XAxis{} xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} @@ -23,17 +23,17 @@ func TestXAxisGetTicks(t *testing.T) { } vf := FloatValueFormatter ticks := xa.GetTicks(r, xr, styleDefaults, vf) - testutil.AssertLen(t, ticks, 16) + assert.Len(ticks, 16) } func TestXAxisGetTicksWithUserDefaults(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) xa := XAxis{ Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, @@ -45,23 +45,23 @@ func TestXAxisGetTicksWithUserDefaults(t *testing.T) { } vf := FloatValueFormatter ticks := xa.GetTicks(r, xr, styleDefaults, vf) - testutil.AssertLen(t, ticks, 1) + assert.Len(ticks, 1) } func TestXAxisMeasure(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) style := Style{ Font: f, FontSize: 10.0, } r, err := PNG(100, 100) - testutil.AssertNil(t, err) + assert.Nil(err) ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} xa := XAxis{} xab := xa.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) - testutil.AssertEqual(t, 122, xab.Width()) - testutil.AssertEqual(t, 21, xab.Height()) + assert.Equal(122, xab.Width()) + assert.Equal(21, xab.Height()) } diff --git a/yaxis.go b/yaxis.go index 841fc86..9de4fb6 100644 --- a/yaxis.go +++ b/yaxis.go @@ -2,14 +2,9 @@ package chart import ( "math" -) -// HideYAxis hides a y-axis. -func HideYAxis() YAxis { - return YAxis{ - Style: Hidden(), - } -} + util "github.com/wcharczuk/go-chart/util" +) // YAxis is a veritcal rule of the range. // There can be (2) y-axes; a primary and secondary. @@ -104,27 +99,27 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic ly := canvasBox.Bottom - ra.Translate(v) tb := r.MeasureText(t.Label) - tbh2 := tb.Height() >> 1 + tbh2 := int(tb.Height()) >> 1 finalTextX := tx if ya.AxisType == YAxisSecondary { - finalTextX = tx - tb.Width() + finalTextX = tx - int(tb.Width()) } - maxTextHeight = MaxInt(tb.Height(), maxTextHeight) + maxTextHeight = util.Math.MaxInt(int(tb.Height()), maxTextHeight) if ya.AxisType == YAxisPrimary { minx = canvasBox.Right - maxx = MaxInt(maxx, tx+tb.Width()) + maxx = util.Math.MaxInt(maxx, tx+int(tb.Width())) } else if ya.AxisType == YAxisSecondary { - minx = MinInt(minx, finalTextX) - maxx = MaxInt(maxx, tx) + minx = util.Math.MinInt(minx, finalTextX) + maxx = util.Math.MaxInt(maxx, tx) } - miny = MinInt(miny, ly-tbh2) - maxy = MaxInt(maxy, ly+tbh2) + miny = util.Math.MinInt(miny, ly-tbh2) + maxy = util.Math.MaxInt(maxy, ly+tbh2) } - if !ya.NameStyle.Hidden && len(ya.Name) > 0 { + if ya.NameStyle.Show && len(ya.Name) > 0 { maxx += (DefaultYAxisMargin + maxTextHeight) } @@ -165,18 +160,18 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick tb := Draw.MeasureText(r, t.Label, tickStyle) - if tb.Width() > maxTextWidth { - maxTextWidth = tb.Width() + if int(tb.Width()) > maxTextWidth { + maxTextWidth = int(tb.Width()) } if ya.AxisType == YAxisSecondary { - finalTextX = tx - tb.Width() + finalTextX = tx - int(tb.Width()) } else { finalTextX = tx } if tickStyle.TextRotationDegrees == 0 { - finalTextY = ly + tb.Height()>>1 + finalTextY = ly + int(tb.Height())>>1 } else { finalTextY = ly } @@ -195,7 +190,7 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick } nameStyle := ya.NameStyle.InheritFrom(defaults.InheritFrom(Style{TextRotationDegrees: 90})) - if !ya.NameStyle.Hidden && len(ya.Name) > 0 { + if ya.NameStyle.Show && len(ya.Name) > 0 { nameStyle.GetTextOptions().WriteToRenderer(r) tb := Draw.MeasureText(r, ya.Name, nameStyle) @@ -208,21 +203,21 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick var ty int if nameStyle.TextRotationDegrees == 0 { - ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Width()>>1) + ty = canvasBox.Top + (canvasBox.Height()>>1 - int(tb.Width())>>1) } else { - ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Height()>>1) + ty = canvasBox.Top + (canvasBox.Height()>>1 - int(tb.Height())>>1) } Draw.Text(r, ya.Name, tx, ty, nameStyle) } - if !ya.Zero.Style.Hidden { + if ya.Zero.Style.Show { ya.Zero.Render(r, canvasBox, ra, false, Style{}) } - if !ya.GridMajorStyle.Hidden || !ya.GridMinorStyle.Hidden { + if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show { for _, gl := range ya.GetGridLines(ticks) { - if (gl.IsMinor && !ya.GridMinorStyle.Hidden) || (!gl.IsMinor && !ya.GridMajorStyle.Hidden) { + if (gl.IsMinor && ya.GridMinorStyle.Show) || (!gl.IsMinor && ya.GridMajorStyle.Show) { defaults := ya.GridMajorStyle if gl.IsMinor { defaults = ya.GridMinorStyle diff --git a/yaxis_test.go b/yaxis_test.go index 793a651..86deae5 100644 --- a/yaxis_test.go +++ b/yaxis_test.go @@ -3,17 +3,17 @@ package chart import ( "testing" - "git.smarteching.com/zeni/go-chart/v2/testutil" + "github.com/blendlabs/go-assert" ) func TestYAxisGetTicks(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) ya := YAxis{} yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} @@ -23,17 +23,17 @@ func TestYAxisGetTicks(t *testing.T) { } vf := FloatValueFormatter ticks := ya.GetTicks(r, yr, styleDefaults, vf) - testutil.AssertLen(t, ticks, 32) + assert.Len(ticks, 32) } func TestYAxisGetTicksWithUserDefaults(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) r, err := PNG(1024, 1024) - testutil.AssertNil(t, err) + assert.Nil(err) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) ya := YAxis{ Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, @@ -45,41 +45,41 @@ func TestYAxisGetTicksWithUserDefaults(t *testing.T) { } vf := FloatValueFormatter ticks := ya.GetTicks(r, yr, styleDefaults, vf) - testutil.AssertLen(t, ticks, 1) + assert.Len(ticks, 1) } func TestYAxisMeasure(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) style := Style{ Font: f, FontSize: 10.0, } r, err := PNG(100, 100) - testutil.AssertNil(t, err) + assert.Nil(err) ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} ya := YAxis{} yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) - testutil.AssertEqual(t, 32, yab.Width()) - testutil.AssertEqual(t, 110, yab.Height()) + assert.Equal(32, yab.Width()) + assert.Equal(110, yab.Height()) } func TestYAxisSecondaryMeasure(t *testing.T) { - // replaced new assertions helper + assert := assert.New(t) f, err := GetDefaultFont() - testutil.AssertNil(t, err) + assert.Nil(err) style := Style{ Font: f, FontSize: 10.0, } r, err := PNG(100, 100) - testutil.AssertNil(t, err) + assert.Nil(err) ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} ya := YAxis{AxisType: YAxisSecondary} yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) - testutil.AssertEqual(t, 32, yab.Width()) - testutil.AssertEqual(t, 110, yab.Height()) + assert.Equal(32, yab.Width()) + assert.Equal(110, yab.Height()) }