Adding upstream version 2.1.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
33
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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 ./...
|
20
.gitignore
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# 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
|
1
COVERAGE
Normal file
|
@ -0,0 +1 @@
|
||||||
|
29.02
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 William Charczuk.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
10
Makefile
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
all: new-install test
|
||||||
|
|
||||||
|
new-install:
|
||||||
|
@go get -v -u ./...
|
||||||
|
|
||||||
|
generate:
|
||||||
|
@go generate ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test ./...
|
4
PROFANITY_RULES.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
go-sdk:
|
||||||
|
excludeFiles: [ "*_test.go" ]
|
||||||
|
importsContain: [ github.com/blend/go-sdk/* ]
|
||||||
|
description: "please don't use go-sdk in this repo"
|
95
README.md
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
go-chart
|
||||||
|
========
|
||||||
|
[](https://github.com/wcharczuk/go-chart/actions/workflows/ci.yml) [](https://goreportcard.com/report/github.com/wcharczuk/go-chart)
|
||||||
|
|
||||||
|
Package `chart` is a very simple golang native charting library that supports timeseries and continuous line charts.
|
||||||
|
|
||||||
|
Master should now be on the v3.x codebase, which overhauls the api significantly. Per usual, see `examples` for more information.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
To install `chart` run the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> go get github.com/wcharczuk/go-chart/v2@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Most of the components are interchangeable so feel free to crib whatever you want.
|
||||||
|
|
||||||
|
# Output Examples
|
||||||
|
|
||||||
|
Spark Lines:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Single axis:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Two axis:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Other Chart Types
|
||||||
|
|
||||||
|
Pie Chart:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The code for this chart can be found in `examples/pie_chart/main.go`.
|
||||||
|
|
||||||
|
Stacked Bar:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Everything starts with the `chart.Chart` object. The bare minimum to draw a chart would be the following:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
...
|
||||||
|
"bytes"
|
||||||
|
...
|
||||||
|
"github.com/wcharczuk/go-chart/v2" //exposes "chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
|
err := graph.Render(chart.PNG, buffer)
|
||||||
|
```
|
||||||
|
|
||||||
|
Explanation of the above: A `chart` can have many `Series`, a `Series` is a collection of things that need to be drawn according to the X range and the Y range(s).
|
||||||
|
|
||||||
|
Here, we have a single series with x range values as float64s, rendered to a PNG. Note; we can pass any type of `io.Writer` into `Render(...)`, meaning that we can render the chart to a file or a resonse or anything else that implements `io.Writer`.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
The best way to see the api in action is to look at the examples in the `./_examples/` directory.
|
||||||
|
|
||||||
|
# Design Philosophy
|
||||||
|
|
||||||
|
I wanted to make a charting library that used only native golang, that could be stood up on a server (i.e. it had built in fonts).
|
||||||
|
|
||||||
|
The goal with the API itself is to have the "zero value be useful", and to require the user to not code more than they absolutely needed.
|
||||||
|
|
||||||
|
# Contributions
|
||||||
|
|
||||||
|
Contributions are welcome though this library is in a holding pattern for the forseable future.
|
147
_colors/colors_extended.txt
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
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
|
BIN
_images/bar_chart.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
_images/goog_ltm.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
_images/ma_goog_ltm.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
_images/pie_chart.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
_images/spy_ltm_bbs.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
_images/stacked_bar.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
_images/tvix_ltm.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
_images/two_axis.png
Normal file
After Width: | Height: | Size: 88 KiB |
91
annotation_series.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*AnnotationSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnotationSeries is a series of labels on the chart.
|
||||||
|
type AnnotationSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
Annotations []Value2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (as AnnotationSeries) GetName() string {
|
||||||
|
return as.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (as AnnotationSeries) GetStyle() Style {
|
||||||
|
return as.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (as AnnotationSeries) GetYAxis() YAxisType {
|
||||||
|
return as.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style {
|
||||||
|
return Style{
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
Font: defaults.Font,
|
||||||
|
FillColor: DefaultAnnotationFillColor,
|
||||||
|
FontSize: DefaultAnnotationFontSize,
|
||||||
|
StrokeColor: defaults.StrokeColor,
|
||||||
|
StrokeWidth: defaults.StrokeWidth,
|
||||||
|
Padding: DefaultAnnotationPadding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure returns a bounds box of the series.
|
||||||
|
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
||||||
|
box := Box{
|
||||||
|
Top: math.MaxInt32,
|
||||||
|
Left: math.MaxInt32,
|
||||||
|
Right: 0,
|
||||||
|
Bottom: 0,
|
||||||
|
}
|
||||||
|
if !as.Style.Hidden {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render draws the series.
|
||||||
|
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
if !as.Style.Hidden {
|
||||||
|
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)
|
||||||
|
Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (as AnnotationSeries) Validate() error {
|
||||||
|
if len(as.Annotations) == 0 {
|
||||||
|
return fmt.Errorf("annotation series requires annotations to be set and not empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
115
annotation_series_test.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
as := AnnotationSeries{
|
||||||
|
Annotations: []Value2{
|
||||||
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
|
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := PNG(110, 110)
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
xrange := &ContinuousRange{
|
||||||
|
Min: 1.0,
|
||||||
|
Max: 4.0,
|
||||||
|
Domain: 100,
|
||||||
|
}
|
||||||
|
yrange := &ContinuousRange{
|
||||||
|
Min: 1.0,
|
||||||
|
Max: 4.0,
|
||||||
|
Domain: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 105,
|
||||||
|
Bottom: 105,
|
||||||
|
}
|
||||||
|
sd := Style{
|
||||||
|
FontSize: 10.0,
|
||||||
|
Font: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnnotationSeriesRender(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
as := AnnotationSeries{
|
||||||
|
Style: Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
},
|
||||||
|
Annotations: []Value2{
|
||||||
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
|
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := PNG(110, 110)
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
xrange := &ContinuousRange{
|
||||||
|
Min: 1.0,
|
||||||
|
Max: 4.0,
|
||||||
|
Domain: 100,
|
||||||
|
}
|
||||||
|
yrange := &ContinuousRange{
|
||||||
|
Min: 1.0,
|
||||||
|
Max: 4.0,
|
||||||
|
Domain: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 105,
|
||||||
|
Bottom: 105,
|
||||||
|
}
|
||||||
|
sd := Style{
|
||||||
|
FontSize: 10.0,
|
||||||
|
Font: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
as.Render(r, cb, xrange, yrange, sd)
|
||||||
|
|
||||||
|
rr, isRaster := r.(*rasterRenderer)
|
||||||
|
testutil.AssertTrue(t, isRaster)
|
||||||
|
testutil.AssertNotNil(t, 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)
|
||||||
|
}
|
24
array.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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]
|
||||||
|
}
|
45
axis.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// TickPosition is an enumeration of possible tick drawing positions.
|
||||||
|
type TickPosition int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TickPositionUnset means to use the default tick position.
|
||||||
|
TickPositionUnset TickPosition = 0
|
||||||
|
// TickPositionBetweenTicks draws the labels for a tick between the previous and current tick.
|
||||||
|
TickPositionBetweenTicks TickPosition = 1
|
||||||
|
// TickPositionUnderTick draws the tick below the tick.
|
||||||
|
TickPositionUnderTick TickPosition = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// YAxisType is a type of y-axis; it can either be primary or secondary.
|
||||||
|
type YAxisType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// YAxisPrimary is the primary axis.
|
||||||
|
YAxisPrimary YAxisType = 0
|
||||||
|
// YAxisSecondary is the secondary axis.
|
||||||
|
YAxisSecondary YAxisType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Axis is a chart feature detailing what values happen where.
|
||||||
|
type Axis interface {
|
||||||
|
GetName() string
|
||||||
|
SetName(name string)
|
||||||
|
|
||||||
|
GetStyle() Style
|
||||||
|
SetStyle(style Style)
|
||||||
|
|
||||||
|
GetTicks() []Tick
|
||||||
|
GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
|
||||||
|
|
||||||
|
// GenerateGridLines returns the gridlines for the axis.
|
||||||
|
GetGridLines(ticks []Tick) []GridLine
|
||||||
|
|
||||||
|
// Measure should return an absolute box for the axis.
|
||||||
|
// This is used when auto-fitting the canvas to the background.
|
||||||
|
Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box
|
||||||
|
|
||||||
|
// Render renders the axis.
|
||||||
|
Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick)
|
||||||
|
}
|
491
bar_chart.go
Normal file
|
@ -0,0 +1,491 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BarChart is a chart that draws bars on a range.
|
||||||
|
type BarChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
BarWidth int
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis Style
|
||||||
|
YAxis YAxis
|
||||||
|
|
||||||
|
BarSpacing int
|
||||||
|
|
||||||
|
UseBaseValue bool
|
||||||
|
BaseValue float64
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Bars []Value
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (bc BarChart) GetDPI() float64 {
|
||||||
|
if bc.DPI == 0 {
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return bc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (bc BarChart) GetFont() *truetype.Font {
|
||||||
|
if bc.Font == nil {
|
||||||
|
return bc.defaultFont
|
||||||
|
}
|
||||||
|
return bc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (bc BarChart) GetWidth() int {
|
||||||
|
if bc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return bc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (bc BarChart) GetHeight() int {
|
||||||
|
if bc.Height == 0 {
|
||||||
|
return DefaultChartHeight
|
||||||
|
}
|
||||||
|
return bc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarSpacing returns the spacing between bars.
|
||||||
|
func (bc BarChart) GetBarSpacing() int {
|
||||||
|
if bc.BarSpacing == 0 {
|
||||||
|
return DefaultBarSpacing
|
||||||
|
}
|
||||||
|
return bc.BarSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarWidth returns the default bar width.
|
||||||
|
func (bc BarChart) GetBarWidth() int {
|
||||||
|
if bc.BarWidth == 0 {
|
||||||
|
return DefaultBarWidth
|
||||||
|
}
|
||||||
|
return bc.BarWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(bc.Bars) == 0 {
|
||||||
|
return errors.New("please provide at least one bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(bc.GetWidth(), bc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(bc.GetDPI())
|
||||||
|
|
||||||
|
bc.drawBackground(r)
|
||||||
|
|
||||||
|
var canvasBox Box
|
||||||
|
var yt []Tick
|
||||||
|
var yr Range
|
||||||
|
var yf ValueFormatter
|
||||||
|
|
||||||
|
canvasBox = bc.getDefaultCanvasBox()
|
||||||
|
yr = bc.getRanges()
|
||||||
|
if yr.GetMax()-yr.GetMin() == 0 {
|
||||||
|
return fmt.Errorf("invalid data range; cannot be zero")
|
||||||
|
}
|
||||||
|
yr = bc.setRangeDomains(canvasBox, yr)
|
||||||
|
yf = bc.getValueFormatters()
|
||||||
|
|
||||||
|
if bc.hasAxes() {
|
||||||
|
yt = bc.getAxesTicks(r, yr, yf)
|
||||||
|
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)
|
||||||
|
|
||||||
|
bc.drawTitle(r)
|
||||||
|
for _, a := range bc.Elements {
|
||||||
|
a(r, canvasBox, bc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
yrange = bc.YAxis.Range
|
||||||
|
} else {
|
||||||
|
yrange = &ContinuousRange{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !yrange.IsZero() {
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bc.YAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range bc.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrange.SetMin(tickMin)
|
||||||
|
yrange.SetMax(tickMax)
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, b := range bc.Bars {
|
||||||
|
min = math.Min(b.Value, min)
|
||||||
|
max = math.Max(b.Value, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
yrange.SetMin(min)
|
||||||
|
yrange.SetMax(max)
|
||||||
|
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: bc.GetWidth(),
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}, bc.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
||||||
|
xoffset := canvasBox.Left
|
||||||
|
|
||||||
|
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
bs2 := spacing >> 1
|
||||||
|
|
||||||
|
var barBox Box
|
||||||
|
var bxl, bxr, by int
|
||||||
|
for index, bar := range bc.Bars {
|
||||||
|
bxl = xoffset + bs2
|
||||||
|
bxr = bxl + width
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
|
||||||
|
|
||||||
|
xoffset += width + spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !bc.XAxis.Hidden {
|
||||||
|
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for index, bar := range bc.Bars {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + width + spacing,
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bar.Label) > 0 {
|
||||||
|
Draw.TextWithin(r, bar.Label, barLabelBox, axisStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
if index < len(bc.Bars)-1 {
|
||||||
|
r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
cursor += width + spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) hasAxes() bool {
|
||||||
|
return !bc.YAxis.Style.Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
|
||||||
|
yr.SetDomain(canvasBox.Height())
|
||||||
|
return yr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getDefaultCanvasBox() Box {
|
||||||
|
return bc.box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getValueFormatters() ValueFormatter {
|
||||||
|
if bc.YAxis.ValueFormatter != nil {
|
||||||
|
return bc.YAxis.ValueFormatter
|
||||||
|
}
|
||||||
|
return FloatValueFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
|
||||||
|
if !bc.YAxis.Style.Hidden {
|
||||||
|
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateEffectiveBarSpacing(canvasBox Box) int {
|
||||||
|
totalWithBaseSpacing := bc.calculateTotalBarWidth(bc.GetBarWidth(), bc.GetBarSpacing())
|
||||||
|
if totalWithBaseSpacing > canvasBox.Width() {
|
||||||
|
lessBarWidths := canvasBox.Width() - (len(bc.Bars) * bc.GetBarWidth())
|
||||||
|
if lessBarWidths > 0 {
|
||||||
|
return int(math.Ceil(float64(lessBarWidths) / float64(len(bc.Bars))))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return bc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateEffectiveBarWidth(canvasBox Box, spacing int) int {
|
||||||
|
totalWithBaseWidth := bc.calculateTotalBarWidth(bc.GetBarWidth(), spacing)
|
||||||
|
if totalWithBaseWidth > canvasBox.Width() {
|
||||||
|
totalLessBarSpacings := canvasBox.Width() - (len(bc.Bars) * spacing)
|
||||||
|
if totalLessBarSpacings > 0 {
|
||||||
|
return int(math.Ceil(float64(totalLessBarSpacings) / float64(len(bc.Bars))))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return bc.GetBarWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateTotalBarWidth(barWidth, spacing int) int {
|
||||||
|
return len(bc.Bars) * (barWidth + spacing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateScaledTotalWidth(canvasBox Box) (width, spacing, total int) {
|
||||||
|
spacing = bc.calculateEffectiveBarSpacing(canvasBox)
|
||||||
|
width = bc.calculateEffectiveBarWidth(canvasBox, spacing)
|
||||||
|
total = bc.calculateTotalBarWidth(width, spacing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, yticks []Tick) Box {
|
||||||
|
axesOuterBox := canvasBox.Clone()
|
||||||
|
|
||||||
|
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
|
||||||
|
if !bc.XAxis.Hidden {
|
||||||
|
xaxisHeight := DefaultVerticalTickHeight
|
||||||
|
|
||||||
|
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range bc.Bars {
|
||||||
|
if len(bar.Label) > 0 {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bc.GetBarWidth() + bc.GetBarSpacing(),
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}
|
||||||
|
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
|
xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xbox := Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: bc.GetHeight() - xaxisHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
axesOuterBox = axesOuterBox.Grow(xbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bc.YAxis.Style.Hidden {
|
||||||
|
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(bc.box(), axesOuterBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
// box returns the chart bounds as a box.
|
||||||
|
func (bc BarChart) box() Box {
|
||||||
|
dpr := bc.Background.Padding.GetRight(10)
|
||||||
|
dpb := bc.Background.Padding.GetBottom(50)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: bc.Background.Padding.GetTop(20),
|
||||||
|
Left: bc.Background.Padding.GetLeft(20),
|
||||||
|
Right: bc.GetWidth() - dpr,
|
||||||
|
Bottom: bc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getBackgroundStyle() Style {
|
||||||
|
return bc.Background.InheritFrom(bc.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: bc.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsBar(index int) Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
StrokeWidth: 3.0,
|
||||||
|
FillColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsTitle() Style {
|
||||||
|
return bc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: bc.GetColorPalette().TextColor(),
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
FontSize: bc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(bc.GetWidth(), bc.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
FontColor: bc.GetColorPalette().TextColor(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (bc BarChart) GetColorPalette() ColorPalette {
|
||||||
|
if bc.ColorPalette != nil {
|
||||||
|
return bc.ColorPalette
|
||||||
|
}
|
||||||
|
return AlternateColorPalette
|
||||||
|
}
|
310
bar_chart_test.go
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBarChartRender(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Width: 1024,
|
||||||
|
Title: "Test Title",
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0, Label: "One"},
|
||||||
|
{Value: 2.0, Label: "Two"},
|
||||||
|
{Value: 3.0, Label: "Three"},
|
||||||
|
{Value: 4.0, Label: "Four"},
|
||||||
|
{Value: 5.0, Label: "Five"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
err := bc.Render(PNG, buf)
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
testutil.AssertNotZero(t, buf.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartRenderZero(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Width: 1024,
|
||||||
|
Title: "Test Title",
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 0.0, Label: "One"},
|
||||||
|
{Value: 0.0, Label: "Two"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
err := bc.Render(PNG, buf)
|
||||||
|
testutil.AssertNotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartProps(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, DefaultDPI, bc.GetDPI())
|
||||||
|
bc.DPI = 100
|
||||||
|
testutil.AssertEqual(t, 100, bc.GetDPI())
|
||||||
|
|
||||||
|
testutil.AssertNil(t, bc.GetFont())
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
bc.Font = f
|
||||||
|
testutil.AssertNotNil(t, bc.GetFont())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, DefaultChartWidth, bc.GetWidth())
|
||||||
|
bc.Width = DefaultChartWidth - 1
|
||||||
|
testutil.AssertEqual(t, DefaultChartWidth-1, bc.GetWidth())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, DefaultChartHeight, bc.GetHeight())
|
||||||
|
bc.Height = DefaultChartHeight - 1
|
||||||
|
testutil.AssertEqual(t, DefaultChartHeight-1, bc.GetHeight())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, DefaultBarSpacing, bc.GetBarSpacing())
|
||||||
|
bc.BarSpacing = 150
|
||||||
|
testutil.AssertEqual(t, 150, bc.GetBarSpacing())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, DefaultBarWidth, bc.GetBarWidth())
|
||||||
|
bc.BarWidth = 75
|
||||||
|
testutil.AssertEqual(t, 75, bc.GetBarWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartRenderNoBars(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
|
||||||
|
testutil.AssertNotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
|
||||||
|
yr := bc.getRanges()
|
||||||
|
testutil.AssertNotNil(t, yr)
|
||||||
|
testutil.AssertFalse(t, yr.IsZero())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, -math.MaxFloat64, yr.GetMax())
|
||||||
|
testutil.AssertEqual(t, math.MaxFloat64, yr.GetMin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetRangesBarsMinMax(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0},
|
||||||
|
{Value: 10.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
yr := bc.getRanges()
|
||||||
|
testutil.AssertNotNil(t, yr)
|
||||||
|
testutil.AssertFalse(t, yr.IsZero())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 10, yr.GetMax())
|
||||||
|
testutil.AssertEqual(t, 1, yr.GetMin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetRangesMinMax(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: 5.0,
|
||||||
|
Max: 15.0,
|
||||||
|
},
|
||||||
|
Ticks: []Tick{
|
||||||
|
{Value: 7.0, Label: "Foo"},
|
||||||
|
{Value: 11.0, Label: "Foo2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0},
|
||||||
|
{Value: 10.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
yr := bc.getRanges()
|
||||||
|
testutil.AssertNotNil(t, yr)
|
||||||
|
testutil.AssertFalse(t, yr.IsZero())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 15, yr.GetMax())
|
||||||
|
testutil.AssertEqual(t, 5, yr.GetMin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetRangesTicksMinMax(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
YAxis: YAxis{
|
||||||
|
Ticks: []Tick{
|
||||||
|
{Value: 7.0, Label: "Foo"},
|
||||||
|
{Value: 11.0, Label: "Foo2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0},
|
||||||
|
{Value: 10.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
yr := bc.getRanges()
|
||||||
|
testutil.AssertNotNil(t, yr)
|
||||||
|
testutil.AssertFalse(t, yr.IsZero())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 11, yr.GetMax())
|
||||||
|
testutil.AssertEqual(t, 7, yr.GetMin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartHasAxes(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
testutil.AssertTrue(t, bc.hasAxes())
|
||||||
|
bc.YAxis = YAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
}
|
||||||
|
testutil.AssertFalse(t, bc.hasAxes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetDefaultCanvasBox(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
b := bc.getDefaultCanvasBox()
|
||||||
|
testutil.AssertFalse(t, b.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartSetRangeDomains(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
cb := bc.box()
|
||||||
|
yr := bc.getRanges()
|
||||||
|
yr2 := bc.setRangeDomains(cb, yr)
|
||||||
|
testutil.AssertNotZero(t, yr2.GetDomain())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetValueFormatters(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{}
|
||||||
|
vf := bc.getValueFormatters()
|
||||||
|
testutil.AssertNotNil(t, vf)
|
||||||
|
testutil.AssertEqual(t, "1234.00", vf(1234.0))
|
||||||
|
|
||||||
|
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
|
||||||
|
testutil.AssertEqual(t, "test", bc.getValueFormatters()(1234))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartGetAxesTicks(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0},
|
||||||
|
{Value: 2.0},
|
||||||
|
{Value: 3.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := PNG(128, 128)
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
yr := bc.getRanges()
|
||||||
|
yf := bc.getValueFormatters()
|
||||||
|
|
||||||
|
bc.YAxis.Style.Hidden = true
|
||||||
|
ticks := bc.getAxesTicks(r, yr, yf)
|
||||||
|
testutil.AssertEmpty(t, ticks)
|
||||||
|
|
||||||
|
bc.YAxis.Style.Hidden = false
|
||||||
|
ticks = bc.getAxesTicks(r, yr, yf)
|
||||||
|
testutil.AssertLen(t, ticks, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Width: 1024,
|
||||||
|
BarWidth: 10,
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0, Label: "One"},
|
||||||
|
{Value: 2.0, Label: "Two"},
|
||||||
|
{Value: 3.0, Label: "Three"},
|
||||||
|
{Value: 4.0, Label: "Four"},
|
||||||
|
{Value: 5.0, Label: "Five"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||||
|
testutil.AssertNotZero(t, spacing)
|
||||||
|
|
||||||
|
bc.BarWidth = 250
|
||||||
|
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||||
|
testutil.AssertZero(t, spacing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
bc := BarChart{
|
||||||
|
Width: 1024,
|
||||||
|
BarWidth: 10,
|
||||||
|
Bars: []Value{
|
||||||
|
{Value: 1.0, Label: "One"},
|
||||||
|
{Value: 2.0, Label: "Two"},
|
||||||
|
{Value: 3.0, Label: "Three"},
|
||||||
|
{Value: 4.0, Label: "Four"},
|
||||||
|
{Value: 5.0, Label: "Five"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := bc.box()
|
||||||
|
|
||||||
|
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||||
|
testutil.AssertNotZero(t, spacing)
|
||||||
|
|
||||||
|
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||||
|
testutil.AssertEqual(t, 10, barWidth)
|
||||||
|
|
||||||
|
bc.BarWidth = 250
|
||||||
|
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||||
|
testutil.AssertZero(t, spacing)
|
||||||
|
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||||
|
testutil.AssertEqual(t, 199, barWidth)
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarChatGetTitleFontSize(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
|
||||||
|
testutil.AssertEqual(t, 48, size)
|
||||||
|
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
|
||||||
|
testutil.AssertEqual(t, 24, size)
|
||||||
|
size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
|
||||||
|
testutil.AssertEqual(t, 18, size)
|
||||||
|
size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
|
||||||
|
testutil.AssertEqual(t, 12, size)
|
||||||
|
size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
|
||||||
|
testutil.AssertEqual(t, 10, size)
|
||||||
|
}
|
135
bollinger_band_series.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*BollingerBandsSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// BollingerBandsSeries draws bollinger bands for an inner series.
|
||||||
|
// Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev.
|
||||||
|
type BollingerBandsSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Period int
|
||||||
|
K float64
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
valueBuffer *ValueBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (bbs BollingerBandsSeries) GetName() string {
|
||||||
|
return bbs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (bbs BollingerBandsSeries) GetStyle() Style {
|
||||||
|
return bbs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (bbs BollingerBandsSeries) GetYAxis() YAxisType {
|
||||||
|
return bbs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (bbs BollingerBandsSeries) GetPeriod() int {
|
||||||
|
if bbs.Period == 0 {
|
||||||
|
return DefaultSimpleMovingAveragePeriod
|
||||||
|
}
|
||||||
|
return bbs.Period
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetK returns the K value, or the number of standard deviations above and below
|
||||||
|
// to band the simple moving average with.
|
||||||
|
// Typical K value is 2.0.
|
||||||
|
func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
|
||||||
|
if bbs.K == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 2.0
|
||||||
|
}
|
||||||
|
return bbs.K
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (bbs BollingerBandsSeries) Len() int {
|
||||||
|
return bbs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedValues gets the bounded value for the series.
|
||||||
|
func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bbs.valueBuffer == nil || index == 0 {
|
||||||
|
bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
|
||||||
|
}
|
||||||
|
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
|
||||||
|
bbs.valueBuffer.Dequeue()
|
||||||
|
}
|
||||||
|
px, py := bbs.InnerSeries.GetValues(index)
|
||||||
|
bbs.valueBuffer.Enqueue(py)
|
||||||
|
x = px
|
||||||
|
|
||||||
|
ay := Seq{bbs.valueBuffer}.Average()
|
||||||
|
std := Seq{bbs.valueBuffer}.StdDev()
|
||||||
|
|
||||||
|
y1 = ay + (bbs.GetK() * std)
|
||||||
|
y2 = ay - (bbs.GetK() * std)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedLastValues returns the last bounded value for the series.
|
||||||
|
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
period := bbs.GetPeriod()
|
||||||
|
seriesLength := bbs.InnerSeries.Len()
|
||||||
|
startAt := seriesLength - period
|
||||||
|
if startAt < 0 {
|
||||||
|
startAt = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
vb := NewValueBufferWithCapacity(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()
|
||||||
|
|
||||||
|
y1 = ay + (bbs.GetK() * std)
|
||||||
|
y2 = ay - (bbs.GetK() * std)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{
|
||||||
|
StrokeWidth: 1.0,
|
||||||
|
StrokeColor: DefaultAxisColor.WithAlpha(64),
|
||||||
|
FillColor: DefaultAxisColor.WithAlpha(32),
|
||||||
|
}))
|
||||||
|
|
||||||
|
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (bbs BollingerBandsSeries) Validate() error {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("bollinger bands series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
52
bollinger_band_series_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBollingerBandSeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
s1 := mockValuesProvider{
|
||||||
|
X: LinearRange(1.0, 100.0),
|
||||||
|
Y: RandomValuesWithMax(100, 1024),
|
||||||
|
}
|
||||||
|
|
||||||
|
bbs := &BollingerBandsSeries{
|
||||||
|
InnerSeries: s1,
|
||||||
|
}
|
||||||
|
|
||||||
|
xvalues := make([]float64, 100)
|
||||||
|
y1values := make([]float64, 100)
|
||||||
|
y2values := make([]float64, 100)
|
||||||
|
|
||||||
|
for x := 0; x < 100; x++ {
|
||||||
|
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := bbs.GetPeriod(); x < 100; x++ {
|
||||||
|
testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBollingerBandLastValue(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
s1 := mockValuesProvider{
|
||||||
|
X: LinearRange(1.0, 100.0),
|
||||||
|
Y: LinearRange(1.0, 100.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
bbs := &BollingerBandsSeries{
|
||||||
|
InnerSeries: s1,
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y1, y2 := bbs.GetBoundedLastValues()
|
||||||
|
testutil.AssertEqual(t, 100.0, x)
|
||||||
|
testutil.AssertEqual(t, 101, math.Floor(y1))
|
||||||
|
testutil.AssertEqual(t, 83, math.Floor(y2))
|
||||||
|
}
|
36
bounded_last_values_annotation_series.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
367
box.go
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// BoxZero is a preset box that represents an intentional zero value.
|
||||||
|
BoxZero = Box{IsSet: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBox returns a new (set) box.
|
||||||
|
func NewBox(top, left, right, bottom int) Box {
|
||||||
|
return Box{
|
||||||
|
IsSet: true,
|
||||||
|
Top: top,
|
||||||
|
Left: left,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box represents the main 4 dimensions of a box.
|
||||||
|
type Box struct {
|
||||||
|
Top int
|
||||||
|
Left int
|
||||||
|
Right int
|
||||||
|
Bottom int
|
||||||
|
IsSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the box is set or not.
|
||||||
|
func (b Box) IsZero() bool {
|
||||||
|
if b.IsSet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the box.
|
||||||
|
func (b Box) String() string {
|
||||||
|
return fmt.Sprintf("box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTop returns a coalesced value with a default.
|
||||||
|
func (b Box) GetTop(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Top == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Top
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeft returns a coalesced value with a default.
|
||||||
|
func (b Box) GetLeft(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Left == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Left
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRight returns a coalesced value with a default.
|
||||||
|
func (b Box) GetRight(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Right == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBottom returns a coalesced value with a default.
|
||||||
|
func (b Box) GetBottom(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Bottom == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width
|
||||||
|
func (b Box) Width() int {
|
||||||
|
return AbsInt(b.Right - b.Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height
|
||||||
|
func (b Box) Height() int {
|
||||||
|
return AbsInt(b.Bottom - b.Top)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center returns the center of the box
|
||||||
|
func (b Box) Center() (x, y int) {
|
||||||
|
w2, h2 := b.Width()>>1, b.Height()>>1
|
||||||
|
return b.Left + w2, b.Top + h2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspect returns the aspect ratio of the box.
|
||||||
|
func (b Box) Aspect() float64 {
|
||||||
|
return float64(b.Width()) / float64(b.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a new copy of the box.
|
||||||
|
func (b Box) Clone() Box {
|
||||||
|
return Box{
|
||||||
|
IsSet: b.IsSet,
|
||||||
|
Top: b.Top,
|
||||||
|
Left: b.Left,
|
||||||
|
Right: b.Right,
|
||||||
|
Bottom: b.Bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBiggerThan returns if a box is bigger than another box.
|
||||||
|
func (b Box) IsBiggerThan(other Box) bool {
|
||||||
|
return b.Top < other.Top ||
|
||||||
|
b.Bottom > other.Bottom ||
|
||||||
|
b.Left < other.Left ||
|
||||||
|
b.Right > other.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSmallerThan returns if a box is smaller than another box.
|
||||||
|
func (b Box) IsSmallerThan(other Box) bool {
|
||||||
|
return b.Top > other.Top &&
|
||||||
|
b.Bottom < other.Bottom &&
|
||||||
|
b.Left > other.Left &&
|
||||||
|
b.Right < other.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if the box equals another box.
|
||||||
|
func (b Box) Equals(other Box) bool {
|
||||||
|
return b.Top == other.Top &&
|
||||||
|
b.Left == other.Left &&
|
||||||
|
b.Right == other.Right &&
|
||||||
|
b.Bottom == other.Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift pushes a box by x,y.
|
||||||
|
func (b Box) Shift(x, y int) Box {
|
||||||
|
return Box{
|
||||||
|
Top: b.Top + y,
|
||||||
|
Left: b.Left + x,
|
||||||
|
Right: b.Right + x,
|
||||||
|
Bottom: b.Bottom + y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fit is functionally the inverse of grow.
|
||||||
|
// Fit maintains the original aspect ratio of the `other` box,
|
||||||
|
// but constrains it to the bounds of the target box.
|
||||||
|
func (b Box) Fit(other Box) Box {
|
||||||
|
ba := b.Aspect()
|
||||||
|
oa := other.Aspect()
|
||||||
|
|
||||||
|
if oa == ba {
|
||||||
|
return b.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
bw, bh := float64(b.Width()), float64(b.Height())
|
||||||
|
bw2 := int(bw) >> 1
|
||||||
|
bh2 := int(bh) >> 1
|
||||||
|
if oa > ba { // ex. 16:9 vs. 4:3
|
||||||
|
var noh2 int
|
||||||
|
if oa > 1.0 {
|
||||||
|
noh2 = int(bw/oa) >> 1
|
||||||
|
} else {
|
||||||
|
noh2 = int(bh*oa) >> 1
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: (b.Top + bh2) - noh2,
|
||||||
|
Left: b.Left,
|
||||||
|
Right: b.Right,
|
||||||
|
Bottom: (b.Top + bh2) + noh2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var now2 int
|
||||||
|
if oa > 1.0 {
|
||||||
|
now2 = int(bh/oa) >> 1
|
||||||
|
} else {
|
||||||
|
now2 = int(bw*oa) >> 1
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: b.Top,
|
||||||
|
Left: (b.Left + bw2) - now2,
|
||||||
|
Right: (b.Left + bw2) + now2,
|
||||||
|
Bottom: b.Bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constrain is similar to `Fit` except that it will work
|
||||||
|
// more literally like the opposite of grow.
|
||||||
|
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)
|
||||||
|
|
||||||
|
return newBox
|
||||||
|
}
|
||||||
|
|
||||||
|
// OuterConstrain is similar to `Constraint` with the difference
|
||||||
|
// that it applies corrections
|
||||||
|
func (b Box) OuterConstrain(bounds, other Box) Box {
|
||||||
|
newBox := b.Clone()
|
||||||
|
if other.Top < bounds.Top {
|
||||||
|
delta := bounds.Top - other.Top
|
||||||
|
newBox.Top = b.Top + delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Left < bounds.Left {
|
||||||
|
delta := bounds.Left - other.Left
|
||||||
|
newBox.Left = b.Left + delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Right > bounds.Right {
|
||||||
|
delta := other.Right - bounds.Right
|
||||||
|
newBox.Right = b.Right - delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Bottom > bounds.Bottom {
|
||||||
|
delta := other.Bottom - bounds.Bottom
|
||||||
|
newBox.Bottom = b.Bottom - delta
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
188
box_test.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoxClone(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxEquals(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxIsBiggerThan(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxIsSmallerThan(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxGrow(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxFit(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxConstrain(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxOuterConstrain(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxShift(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
b := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 10,
|
||||||
|
Bottom: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoxCenter(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
b := Box{
|
||||||
|
Top: 10,
|
||||||
|
Left: 10,
|
||||||
|
Right: 20,
|
||||||
|
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())
|
||||||
|
}
|
577
chart.go
Normal file
|
@ -0,0 +1,577 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chart is what we're drawing.
|
||||||
|
type Chart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis XAxis
|
||||||
|
YAxis YAxis
|
||||||
|
YAxisSecondary YAxis
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Series []Series
|
||||||
|
Elements []Renderable
|
||||||
|
|
||||||
|
Log Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (c Chart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if c.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return c.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (c Chart) GetFont() *truetype.Font {
|
||||||
|
if c.Font == nil {
|
||||||
|
return c.defaultFont
|
||||||
|
}
|
||||||
|
return c.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (c Chart) GetWidth() int {
|
||||||
|
if c.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return c.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (c Chart) GetHeight() int {
|
||||||
|
if c.Height == 0 {
|
||||||
|
return DefaultChartHeight
|
||||||
|
}
|
||||||
|
return c.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
c.YAxisSecondary.AxisType = YAxisSecondary
|
||||||
|
|
||||||
|
r, err := rp(c.GetWidth(), c.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(c.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
c.drawBackground(r)
|
||||||
|
|
||||||
|
var xt, yt, yta []Tick
|
||||||
|
xr, yr, yra := c.getRanges()
|
||||||
|
canvasBox := c.getDefaultCanvasBox()
|
||||||
|
xf, yf, yfa := c.getValueFormatters()
|
||||||
|
|
||||||
|
Debugf(c.Log, "chart; canvas box: %v", canvasBox)
|
||||||
|
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
|
||||||
|
err = c.checkRanges(xr, yr, yra)
|
||||||
|
if err != nil {
|
||||||
|
r.Save(w)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasAxes() {
|
||||||
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
|
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)
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasAnnotationSeries() {
|
||||||
|
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)
|
||||||
|
c.drawAxes(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
|
for index, series := range c.Series {
|
||||||
|
c.drawSeries(r, canvasBox, xr, yr, yra, series, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.drawTitle(r)
|
||||||
|
|
||||||
|
for _, a := range c.Elements {
|
||||||
|
a(r, canvasBox, c.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) checkHasVisibleSeries() error {
|
||||||
|
var style Style
|
||||||
|
for _, s := range c.Series {
|
||||||
|
style = s.GetStyle()
|
||||||
|
if !style.Hidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("chart render; must have (1) visible series")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) validateSeries() error {
|
||||||
|
var err error
|
||||||
|
for _, s := range c.Series {
|
||||||
|
err = s.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
|
var minx, maxx float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
var miny, maxy float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
var minya, maxya float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
|
||||||
|
seriesMappedToSecondaryAxis := false
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
seriesAxis := s.GetYAxis()
|
||||||
|
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
|
||||||
|
seriesLength := bvp.Len()
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy1, vy2 := bvp.GetBoundedValues(index)
|
||||||
|
|
||||||
|
minx = math.Min(minx, vx)
|
||||||
|
maxx = math.Max(maxx, vx)
|
||||||
|
|
||||||
|
if seriesAxis == YAxisPrimary {
|
||||||
|
miny = math.Min(miny, vy1)
|
||||||
|
miny = math.Min(miny, vy2)
|
||||||
|
maxy = math.Max(maxy, vy1)
|
||||||
|
maxy = math.Max(maxy, vy2)
|
||||||
|
} else if seriesAxis == YAxisSecondary {
|
||||||
|
minya = math.Min(minya, vy1)
|
||||||
|
minya = math.Min(minya, vy2)
|
||||||
|
maxya = math.Max(maxya, vy1)
|
||||||
|
maxya = math.Max(maxya, vy2)
|
||||||
|
seriesMappedToSecondaryAxis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
seriesLength := vp.Len()
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy := vp.GetValues(index)
|
||||||
|
|
||||||
|
minx = math.Min(minx, vx)
|
||||||
|
maxx = math.Max(maxx, vx)
|
||||||
|
|
||||||
|
if seriesAxis == YAxisPrimary {
|
||||||
|
miny = math.Min(miny, vy)
|
||||||
|
maxy = math.Max(maxy, vy)
|
||||||
|
} else if seriesAxis == YAxisSecondary {
|
||||||
|
minya = math.Min(minya, vy)
|
||||||
|
maxya = math.Max(maxya, vy)
|
||||||
|
seriesMappedToSecondaryAxis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.XAxis.Range == nil {
|
||||||
|
xrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
xrange = c.XAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxis.Range == nil {
|
||||||
|
yrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrange = c.YAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxisSecondary.Range == nil {
|
||||||
|
yrangeAlt = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrangeAlt = c.YAxisSecondary.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.XAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.XAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
xrange.SetMin(tickMin)
|
||||||
|
xrange.SetMax(tickMax)
|
||||||
|
} else if xrange.IsZero() {
|
||||||
|
xrange.SetMin(minx)
|
||||||
|
xrange.SetMax(maxx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.YAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrange.SetMin(tickMin)
|
||||||
|
yrange.SetMax(tickMax)
|
||||||
|
} else if yrange.IsZero() {
|
||||||
|
yrange.SetMin(miny)
|
||||||
|
yrange.SetMax(maxy)
|
||||||
|
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
delta := yrange.GetDelta()
|
||||||
|
roundTo := GetRoundToForDelta(delta)
|
||||||
|
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
|
||||||
|
|
||||||
|
yrange.SetMin(rmin)
|
||||||
|
yrange.SetMax(rmax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.YAxisSecondary.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrangeAlt.SetMin(tickMin)
|
||||||
|
yrangeAlt.SetMax(tickMax)
|
||||||
|
} else if seriesMappedToSecondaryAxis && yrangeAlt.IsZero() {
|
||||||
|
yrangeAlt.SetMin(minya)
|
||||||
|
yrangeAlt.SetMax(maxya)
|
||||||
|
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
delta := yrangeAlt.GetDelta()
|
||||||
|
roundTo := GetRoundToForDelta(delta)
|
||||||
|
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||||
|
yrangeAlt.SetMin(rmin)
|
||||||
|
yrangeAlt.SetMax(rmax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) checkRanges(xr, yr, yra Range) error {
|
||||||
|
Debugf(c.Log, "checking xrange: %v", xr)
|
||||||
|
xDelta := xr.GetDelta()
|
||||||
|
if math.IsInf(xDelta, 0) {
|
||||||
|
return errors.New("infinite x-range delta")
|
||||||
|
}
|
||||||
|
if math.IsNaN(xDelta) {
|
||||||
|
return errors.New("nan x-range delta")
|
||||||
|
}
|
||||||
|
if xDelta == 0 {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
if math.IsNaN(yDelta) {
|
||||||
|
return errors.New("nan 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")
|
||||||
|
}
|
||||||
|
if math.IsNaN(yraDelta) {
|
||||||
|
return errors.New("nan secondary y-range delta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getDefaultCanvasBox() Box {
|
||||||
|
return c.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if vfp, isVfp := s.(ValueFormatterProvider); isVfp {
|
||||||
|
sx, sy := vfp.GetValueFormatters()
|
||||||
|
if s.GetYAxis() == YAxisPrimary {
|
||||||
|
x = sx
|
||||||
|
y = sy
|
||||||
|
} else if s.GetYAxis() == YAxisSecondary {
|
||||||
|
x = sx
|
||||||
|
ya = sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.XAxis.ValueFormatter != nil {
|
||||||
|
x = c.XAxis.GetValueFormatter()
|
||||||
|
}
|
||||||
|
if c.YAxis.ValueFormatter != nil {
|
||||||
|
y = c.YAxis.GetValueFormatter()
|
||||||
|
}
|
||||||
|
if c.YAxisSecondary.ValueFormatter != nil {
|
||||||
|
ya = c.YAxisSecondary.GetValueFormatter()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasAxes() bool {
|
||||||
|
return !c.XAxis.Style.Hidden || !c.YAxis.Style.Hidden || !c.YAxisSecondary.Style.Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) {
|
||||||
|
if !c.XAxis.Style.Hidden {
|
||||||
|
xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf)
|
||||||
|
}
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
|
||||||
|
}
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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() {
|
||||||
|
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt)
|
||||||
|
Debugf(c.Log, "chart; y-axis secondary measured %v", axesBounds)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) {
|
||||||
|
xr.SetDomain(canvasBox.Width())
|
||||||
|
yr.SetDomain(canvasBox.Height())
|
||||||
|
yra.SetDomain(canvasBox.Height())
|
||||||
|
return xr, yr, yra
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasAnnotationSeries() bool {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||||
|
if !as.GetStyle().Hidden {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasSecondarySeries() bool {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if s.GetYAxis() == YAxisSecondary {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xf, yf, yfa ValueFormatter) Box {
|
||||||
|
annotationSeriesBox := canvasBox.Clone()
|
||||||
|
for seriesIndex, s := range c.Series {
|
||||||
|
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||||
|
if !as.GetStyle().Hidden {
|
||||||
|
style := c.styleDefaultsSeries(seriesIndex)
|
||||||
|
var annotationBounds Box
|
||||||
|
if as.YAxis == YAxisPrimary {
|
||||||
|
annotationBounds = as.Measure(r, canvasBox, xr, yr, style)
|
||||||
|
} else if as.YAxis == YAxisSecondary {
|
||||||
|
annotationBounds = as.Measure(r, canvasBox, xr, yra, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationSeriesBox = annotationSeriesBox.Grow(annotationBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(c.Box(), annotationSeriesBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getBackgroundStyle() Style {
|
||||||
|
return c.Background.InheritFrom(c.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: c.GetWidth(),
|
||||||
|
Bottom: c.GetHeight(),
|
||||||
|
}, c.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getCanvasStyle() Style {
|
||||||
|
return c.Canvas.InheritFrom(c.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, c.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
|
||||||
|
if !c.XAxis.Style.Hidden {
|
||||||
|
c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
|
||||||
|
}
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
|
||||||
|
}
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
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.GetYAxis() == YAxisPrimary {
|
||||||
|
s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
|
||||||
|
} else if s.GetYAxis() == YAxisSecondary {
|
||||||
|
s.Render(r, canvasBox, xrange, yrangeAlt, c.styleDefaultsSeries(seriesIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawTitle(r Renderer) {
|
||||||
|
if len(c.Title) > 0 && !c.TitleStyle.Hidden {
|
||||||
|
r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
|
||||||
|
r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
|
||||||
|
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||||||
|
r.SetFontSize(titleFontSize)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(c.Title)
|
||||||
|
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
|
||||||
|
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
|
||||||
|
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||||
|
|
||||||
|
r.Text(c.Title, titleX, titleY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: c.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultBackgroundStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: c.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultCanvasStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||||
|
return Style{
|
||||||
|
DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||||
|
StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||||
|
StrokeWidth: DefaultSeriesLineWidth,
|
||||||
|
Font: c.GetFont(),
|
||||||
|
FontSize: DefaultFontSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
Font: c.GetFont(),
|
||||||
|
FontColor: c.GetColorPalette().TextColor(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
StrokeColor: c.GetColorPalette().AxisStrokeColor(),
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: c.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (c Chart) GetColorPalette() ColorPalette {
|
||||||
|
if c.ColorPalette != nil {
|
||||||
|
return c.ColorPalette
|
||||||
|
}
|
||||||
|
return DefaultColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (c Chart) Box() Box {
|
||||||
|
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||||
|
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
Left: c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
|
||||||
|
Right: c.GetWidth() - dpr,
|
||||||
|
Bottom: c.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
594
chart_test.go
Normal file
|
@ -0,0 +1,594 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChartGetDPI(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
unset := Chart{}
|
||||||
|
testutil.AssertEqual(t, DefaultDPI, unset.GetDPI())
|
||||||
|
testutil.AssertEqual(t, 192, unset.GetDPI(192))
|
||||||
|
|
||||||
|
set := Chart{DPI: 128}
|
||||||
|
testutil.AssertEqual(t, 128, set.GetDPI())
|
||||||
|
testutil.AssertEqual(t, 128, set.GetDPI(192))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetFont(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
unset := Chart{}
|
||||||
|
testutil.AssertNil(t, unset.GetFont())
|
||||||
|
|
||||||
|
set := Chart{Font: f}
|
||||||
|
testutil.AssertNotNil(t, set.GetFont())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetWidth(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
unset := Chart{}
|
||||||
|
testutil.AssertEqual(t, DefaultChartWidth, unset.GetWidth())
|
||||||
|
|
||||||
|
set := Chart{Width: DefaultChartWidth + 10}
|
||||||
|
testutil.AssertEqual(t, DefaultChartWidth+10, set.GetWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetHeight(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
unset := Chart{}
|
||||||
|
testutil.AssertEqual(t, DefaultChartHeight, unset.GetHeight())
|
||||||
|
|
||||||
|
set := Chart{Height: DefaultChartHeight + 10}
|
||||||
|
testutil.AssertEqual(t, DefaultChartHeight+10, set.GetHeight())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
YAxis: YAxisSecondary,
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
xrange, yrange, yrangeAlt := c.getRanges()
|
||||||
|
testutil.AssertEqual(t, -2.0, xrange.GetMin())
|
||||||
|
testutil.AssertEqual(t, 5.0, xrange.GetMax())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, -2.1, yrange.GetMin())
|
||||||
|
testutil.AssertEqual(t, 4.5, yrange.GetMax())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 10.0, yrangeAlt.GetMin())
|
||||||
|
testutil.AssertEqual(t, 14.0, yrangeAlt.GetMax())
|
||||||
|
|
||||||
|
cSet := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||||
|
},
|
||||||
|
YAxisSecondary: YAxis{
|
||||||
|
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
YAxis: YAxisSecondary,
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
xr2, yr2, yra2 := cSet.getRanges()
|
||||||
|
testutil.AssertEqual(t, 9.8, xr2.GetMin())
|
||||||
|
testutil.AssertEqual(t, 19.8, xr2.GetMax())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 9.9, yr2.GetMin())
|
||||||
|
testutil.AssertEqual(t, 19.9, yr2.GetMax())
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, 9.7, yra2.GetMin())
|
||||||
|
testutil.AssertEqual(t, 19.7, yra2.GetMax())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetRangesUseTicks(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
// this test asserts that ticks should supercede manual ranges when generating the overall ranges.
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
YAxis: YAxis{
|
||||||
|
Ticks: []Tick{
|
||||||
|
{0.0, "Zero"},
|
||||||
|
{1.0, "1.0"},
|
||||||
|
{2.0, "2.0"},
|
||||||
|
{3.0, "3.0"},
|
||||||
|
{4.0, "4.0"},
|
||||||
|
{5.0, "Five"},
|
||||||
|
},
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: -5.0,
|
||||||
|
Max: 5.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetRangesUseUserRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: -5.0,
|
||||||
|
Max: 5.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetBackgroundStyle(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Background: Style{
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := c.getBackgroundStyle()
|
||||||
|
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetCanvasStyle(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Canvas: Style{
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := c.getCanvasStyle()
|
||||||
|
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetDefaultCanvasBox(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
custom := Chart{
|
||||||
|
Background: Style{
|
||||||
|
Padding: Box{
|
||||||
|
Top: DefaultBackgroundPadding.Top + 1,
|
||||||
|
Left: DefaultBackgroundPadding.Left + 1,
|
||||||
|
Right: DefaultBackgroundPadding.Right + 1,
|
||||||
|
Bottom: DefaultBackgroundPadding.Bottom + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetValueFormatters(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||||
|
},
|
||||||
|
ContinuousSeries{
|
||||||
|
YAxis: YAxisSecondary,
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dxf, dyf, dyaf := c.getValueFormatters()
|
||||||
|
testutil.AssertNotNil(t, dxf)
|
||||||
|
testutil.AssertNotNil(t, dyf)
|
||||||
|
testutil.AssertNotNil(t, dyaf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartHasAxes(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
testutil.AssertTrue(t, Chart{}.hasAxes())
|
||||||
|
testutil.AssertFalse(t, Chart{XAxis: XAxis{Style: Hidden()}, YAxis: YAxis{Style: Hidden()}, YAxisSecondary: YAxis{Style: Hidden()}}.hasAxes())
|
||||||
|
|
||||||
|
x := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Style: Shown(),
|
||||||
|
},
|
||||||
|
YAxisSecondary: YAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testutil.AssertTrue(t, x.hasAxes())
|
||||||
|
|
||||||
|
y := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Style: Shown(),
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
YAxisSecondary: YAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testutil.AssertTrue(t, y.hasAxes())
|
||||||
|
|
||||||
|
ya := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Style: Hidden(),
|
||||||
|
},
|
||||||
|
YAxisSecondary: YAxis{
|
||||||
|
Style: Shown(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testutil.AssertTrue(t, ya.hasAxes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartGetAxesTicks(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
r, err := PNG(1024, 1024)
|
||||||
|
testutil.AssertNil(t, err)
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||||
|
},
|
||||||
|
YAxisSecondary: YAxis{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartSingleSeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
now := time.Now()
|
||||||
|
c := Chart{
|
||||||
|
Title: "Hello!",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 400,
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: 0.0,
|
||||||
|
Max: 4.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
TimeSeries{
|
||||||
|
Name: "goog",
|
||||||
|
XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
|
c.Render(PNG, buffer)
|
||||||
|
testutil.AssertNotEmpty(t, buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartRegressionBadRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{math.Inf(1), math.Inf(1), math.Inf(1), math.Inf(1), math.Inf(1)},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
|
c.Render(PNG, buffer)
|
||||||
|
testutil.AssertTrue(t, true, "Render needs to finish.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartRegressionBadRangesByUser(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: math.Inf(-1),
|
||||||
|
Max: math.Inf(1), // this could really happen? eh.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
|
c.Render(PNG, buffer)
|
||||||
|
testutil.AssertTrue(t, true, "Render needs to finish.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartValidatesSeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testutil.AssertNil(t, c.validateSeries())
|
||||||
|
|
||||||
|
c = Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testutil.AssertNotNil(t, c.validateSeries())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartCheckRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0},
|
||||||
|
YValues: []float64{3.10, 3.14},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
xr, yr, yra := c.getRanges()
|
||||||
|
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartCheckRangesWithRanges(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
XAxis: XAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: 0,
|
||||||
|
Max: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
YAxis: YAxis{
|
||||||
|
Range: &ContinuousRange{
|
||||||
|
Min: 0,
|
||||||
|
Max: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0},
|
||||||
|
YValues: []float64{3.14, 3.14},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
xr, yr, yra := c.getRanges()
|
||||||
|
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
|
||||||
|
}
|
||||||
|
|
||||||
|
func at(i image.Image, x, y int) drawing.Color {
|
||||||
|
return drawing.ColorFromAlphaMixedRGBA(i.At(x, y).RGBA())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Height: 50,
|
||||||
|
Width: 50,
|
||||||
|
Canvas: Style{
|
||||||
|
Padding: BoxZero,
|
||||||
|
},
|
||||||
|
Background: Style{
|
||||||
|
Padding: BoxZero,
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.ColorRed, 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgContent bytes.Buffer
|
||||||
|
err := poc.Render(PNG, &imgContent)
|
||||||
|
testutil.AssertNotNil(t, err)
|
||||||
|
}
|
148
cmd/chart/main.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
outputPath = flag.String("output", "", "The output file")
|
||||||
|
|
||||||
|
inputFormat = flag.String("format", "csv", "The input format, either 'csv' or 'tsv' (defaults to 'csv')")
|
||||||
|
inputPath = flag.String("f", "", "The input file")
|
||||||
|
reverse = flag.Bool("reverse", false, "If we should reverse the inputs")
|
||||||
|
|
||||||
|
hideLegend = flag.Bool("hide-legend", false, "If we should omit the chart legend")
|
||||||
|
hideSMA = flag.Bool("hide-sma", false, "If we should omit simple moving average")
|
||||||
|
hideLinreg = flag.Bool("hide-linreg", false, "If we should omit linear regressions")
|
||||||
|
hideLastValues = flag.Bool("hide-last-values", false, "If we should omit last values")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
log := chart.NewLogger()
|
||||||
|
|
||||||
|
var rawData []byte
|
||||||
|
var err error
|
||||||
|
if *inputPath != "" {
|
||||||
|
if *inputPath == "-" {
|
||||||
|
rawData, err = ioutil.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rawData, err = ioutil.ReadFile(*inputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(flag.Args()) > 0 {
|
||||||
|
rawData = []byte(flag.Args()[0])
|
||||||
|
} else {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
switch *inputFormat {
|
||||||
|
case "csv":
|
||||||
|
parts = chart.SplitCSV(string(rawData))
|
||||||
|
case "tsv":
|
||||||
|
parts = strings.Split(string(rawData), "\t")
|
||||||
|
default:
|
||||||
|
log.FatalErr(fmt.Errorf("invalid format; must be 'csv' or 'tsv'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
yvalues, err := chart.ParseFloats(parts...)
|
||||||
|
if err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *reverse {
|
||||||
|
yvalues = chart.ValueSequence(yvalues...).Reverse().Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
var series []chart.Series
|
||||||
|
mainSeries := chart.ContinuousSeries{
|
||||||
|
Name: "Values",
|
||||||
|
XValues: chart.LinearRange(1, float64(len(yvalues))),
|
||||||
|
YValues: yvalues,
|
||||||
|
}
|
||||||
|
series = append(series, mainSeries)
|
||||||
|
|
||||||
|
smaSeries := &chart.SMASeries{
|
||||||
|
Name: "SMA",
|
||||||
|
Style: chart.Style{
|
||||||
|
Hidden: *hideSMA,
|
||||||
|
StrokeColor: chart.ColorRed,
|
||||||
|
StrokeDashArray: []float64{5.0, 5.0},
|
||||||
|
},
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
}
|
||||||
|
series = append(series, smaSeries)
|
||||||
|
|
||||||
|
linRegSeries := &chart.LinearRegressionSeries{
|
||||||
|
Name: "Values - Lin. Reg.",
|
||||||
|
Style: chart.Style{
|
||||||
|
Hidden: *hideLinreg,
|
||||||
|
},
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
}
|
||||||
|
series = append(series, linRegSeries)
|
||||||
|
|
||||||
|
mainLastValue := chart.LastValueAnnotationSeries(mainSeries)
|
||||||
|
mainLastValue.Style = chart.Style{
|
||||||
|
Hidden: *hideLastValues,
|
||||||
|
}
|
||||||
|
series = append(series, mainLastValue)
|
||||||
|
|
||||||
|
linregLastValue := chart.LastValueAnnotationSeries(linRegSeries)
|
||||||
|
linregLastValue.Style = chart.Style{
|
||||||
|
Hidden: (*hideLastValues || *hideLinreg),
|
||||||
|
}
|
||||||
|
series = append(series, linregLastValue)
|
||||||
|
|
||||||
|
smaLastValue := chart.LastValueAnnotationSeries(smaSeries)
|
||||||
|
smaLastValue.Style = chart.Style{
|
||||||
|
Hidden: (*hideLastValues || *hideSMA),
|
||||||
|
}
|
||||||
|
series = append(series, smaLastValue)
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
Background: chart.Style{
|
||||||
|
Padding: chart.Box{
|
||||||
|
Top: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: series,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*hideLegend {
|
||||||
|
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var output *os.File
|
||||||
|
if *outputPath != "" {
|
||||||
|
output, err = os.Create(*outputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output, err = ioutil.TempFile("", "*.png")
|
||||||
|
if err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := graph.Render(chart.PNG, output); err != nil {
|
||||||
|
log.FatalErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, output.Name())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
184
colors.go
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ColorWhite is white.
|
||||||
|
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
// ColorBlue is the basic theme blue color.
|
||||||
|
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
|
||||||
|
// ColorCyan is the basic theme cyan color.
|
||||||
|
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
|
||||||
|
// ColorGreen is the basic theme green color.
|
||||||
|
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
|
||||||
|
// ColorRed is the basic theme red color.
|
||||||
|
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
|
||||||
|
// ColorOrange is the basic theme orange color.
|
||||||
|
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
|
||||||
|
// ColorYellow is the basic theme yellow color.
|
||||||
|
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
|
||||||
|
// ColorBlack is the basic theme black color.
|
||||||
|
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
||||||
|
// ColorLightGray is the basic theme light gray color.
|
||||||
|
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
||||||
|
|
||||||
|
// ColorAlternateBlue is a alternate theme color.
|
||||||
|
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
|
||||||
|
// ColorAlternateGreen is a alternate theme color.
|
||||||
|
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
|
||||||
|
// ColorAlternateGray is a alternate theme color.
|
||||||
|
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
|
||||||
|
// ColorAlternateYellow is a alternate theme color.
|
||||||
|
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
|
||||||
|
// ColorAlternateLightGray is a alternate theme color.
|
||||||
|
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
|
||||||
|
|
||||||
|
// ColorTransparent is a transparent (alpha zero) color.
|
||||||
|
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultBackgroundColor is the default chart background color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultBackgroundColor = ColorWhite
|
||||||
|
// DefaultBackgroundStrokeColor is the default chart border color.
|
||||||
|
// It is equivalent to color:white.
|
||||||
|
DefaultBackgroundStrokeColor = ColorWhite
|
||||||
|
// DefaultCanvasColor is the default chart canvas color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasColor = ColorWhite
|
||||||
|
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasStrokeColor = ColorWhite
|
||||||
|
// DefaultTextColor is the default chart text color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultTextColor = ColorBlack
|
||||||
|
// DefaultAxisColor is the default chart axis line color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultAxisColor = ColorBlack
|
||||||
|
// DefaultStrokeColor is the default chart border color.
|
||||||
|
// It is equivalent to #efefef.
|
||||||
|
DefaultStrokeColor = ColorLightGray
|
||||||
|
// DefaultFillColor is the default fill color.
|
||||||
|
// It is equivalent to #0074d9.
|
||||||
|
DefaultFillColor = ColorBlue
|
||||||
|
// DefaultAnnotationFillColor is the default annotation background color.
|
||||||
|
DefaultAnnotationFillColor = ColorWhite
|
||||||
|
// DefaultGridLineColor is the default grid line color.
|
||||||
|
DefaultGridLineColor = ColorLightGray
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultColors are a couple default series colors.
|
||||||
|
DefaultColors = []drawing.Color{
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAlternateColors are a couple alternate colors.
|
||||||
|
DefaultAlternateColors = []drawing.Color{
|
||||||
|
ColorAlternateBlue,
|
||||||
|
ColorAlternateGreen,
|
||||||
|
ColorAlternateGray,
|
||||||
|
ColorAlternateYellow,
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultColor returns a color from the default list by index.
|
||||||
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
|
func GetDefaultColor(index int) drawing.Color {
|
||||||
|
finalIndex := index % len(DefaultColors)
|
||||||
|
return DefaultColors[finalIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlternateColor returns a color from the default list by index.
|
||||||
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
|
func GetAlternateColor(index int) drawing.Color {
|
||||||
|
finalIndex := index % len(DefaultAlternateColors)
|
||||||
|
return DefaultAlternateColors[finalIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorPalette is a set of colors that.
|
||||||
|
type ColorPalette interface {
|
||||||
|
BackgroundColor() drawing.Color
|
||||||
|
BackgroundStrokeColor() drawing.Color
|
||||||
|
CanvasColor() drawing.Color
|
||||||
|
CanvasStrokeColor() drawing.Color
|
||||||
|
AxisStrokeColor() drawing.Color
|
||||||
|
TextColor() drawing.Color
|
||||||
|
GetSeriesColor(index int) drawing.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultColorPalette represents the default palatte.
|
||||||
|
var DefaultColorPalette defaultColorPalette
|
||||||
|
|
||||||
|
type defaultColorPalette struct{}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) BackgroundColor() drawing.Color {
|
||||||
|
return DefaultBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||||
|
return DefaultBackgroundStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) CanvasColor() drawing.Color {
|
||||||
|
return DefaultCanvasColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
|
||||||
|
return DefaultCanvasStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
|
||||||
|
return DefaultAxisColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) TextColor() drawing.Color {
|
||||||
|
return DefaultTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||||
|
return GetDefaultColor(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlternateColorPalette represents the default palatte.
|
||||||
|
var AlternateColorPalette alternateColorPalette
|
||||||
|
|
||||||
|
type alternateColorPalette struct{}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) BackgroundColor() drawing.Color {
|
||||||
|
return DefaultBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||||
|
return DefaultBackgroundStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) CanvasColor() drawing.Color {
|
||||||
|
return DefaultCanvasColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
|
||||||
|
return DefaultCanvasStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
|
||||||
|
return DefaultAxisColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) TextColor() drawing.Color {
|
||||||
|
return DefaultTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||||
|
return GetAlternateColor(index)
|
||||||
|
}
|
44
concat_series.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// ConcatSeries is a special type of series that concatenates its `InnerSeries`.
|
||||||
|
type ConcatSeries []Series
|
||||||
|
|
||||||
|
// Len returns the length of the concatenated set of series.
|
||||||
|
func (cs ConcatSeries) Len() int {
|
||||||
|
total := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
total += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1)
|
||||||
|
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
|
||||||
|
cursor := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
len := typed.Len()
|
||||||
|
if index < cursor+len {
|
||||||
|
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cursor += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (cs ConcatSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
for _, s := range cs {
|
||||||
|
err = s.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
41
concat_series_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConcatSeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
s1 := ContinuousSeries{
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
s2 := ContinuousSeries{
|
||||||
|
XValues: LinearRange(11, 20.0),
|
||||||
|
YValues: LinearRange(10.0, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
s3 := ContinuousSeries{
|
||||||
|
XValues: LinearRange(21, 30.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := ConcatSeries([]Series{s1, s2, s3})
|
||||||
|
testutil.AssertEqual(t, 30, cs.Len())
|
||||||
|
|
||||||
|
x0, y0 := cs.GetValue(0)
|
||||||
|
testutil.AssertEqual(t, 1.0, x0)
|
||||||
|
testutil.AssertEqual(t, 1.0, y0)
|
||||||
|
|
||||||
|
xm, ym := cs.GetValue(19)
|
||||||
|
testutil.AssertEqual(t, 20.0, xm)
|
||||||
|
testutil.AssertEqual(t, 1.0, ym)
|
||||||
|
|
||||||
|
xn, yn := cs.GetValue(29)
|
||||||
|
testutil.AssertEqual(t, 30.0, xn)
|
||||||
|
testutil.AssertEqual(t, 10.0, yn)
|
||||||
|
}
|
81
continuous_range.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContinuousRange represents a boundary for a set of numbers.
|
||||||
|
type ContinuousRange struct {
|
||||||
|
Min float64
|
||||||
|
Max float64
|
||||||
|
Domain int
|
||||||
|
Descending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDescending returns if the range is descending.
|
||||||
|
func (r ContinuousRange) IsDescending() bool {
|
||||||
|
return r.Descending
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the ContinuousRange has been set or not.
|
||||||
|
func (r ContinuousRange) 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 ContinuousRange) GetMin() float64 {
|
||||||
|
return r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMin sets the min value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMin(min float64) {
|
||||||
|
r.Min = min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMax returns the max value for the continuous range.
|
||||||
|
func (r ContinuousRange) GetMax() float64 {
|
||||||
|
return r.Max
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMax sets the max value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMax(max float64) {
|
||||||
|
r.Max = max
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelta returns the difference between the min and max value.
|
||||||
|
func (r ContinuousRange) GetDelta() float64 {
|
||||||
|
return r.Max - r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain returns the range domain.
|
||||||
|
func (r ContinuousRange) GetDomain() int {
|
||||||
|
return r.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDomain sets the range domain.
|
||||||
|
func (r *ContinuousRange) SetDomain(domain int) {
|
||||||
|
r.Domain = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate maps a given value into the ContinuousRange space.
|
||||||
|
func (r ContinuousRange) Translate(value float64) int {
|
||||||
|
normalized := value - r.Min
|
||||||
|
ratio := normalized / r.GetDelta()
|
||||||
|
|
||||||
|
if r.IsDescending() {
|
||||||
|
return r.Domain - int(math.Ceil(ratio*float64(r.Domain)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(math.Ceil(ratio * float64(r.Domain)))
|
||||||
|
}
|
22
continuous_range_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRangeTranslate(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
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...)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
96
continuous_series.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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
|
||||||
|
Style Style
|
||||||
|
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
XValueFormatter ValueFormatter
|
||||||
|
YValueFormatter ValueFormatter
|
||||||
|
|
||||||
|
XValues []float64
|
||||||
|
YValues []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (cs ContinuousSeries) GetName() string {
|
||||||
|
return cs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (cs ContinuousSeries) GetStyle() Style {
|
||||||
|
return cs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (cs ContinuousSeries) Len() int {
|
||||||
|
return len(cs.XValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets the x,y values at a given index.
|
||||||
|
func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueFormatters returns value formatter defaults for the series.
|
||||||
|
func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
|
||||||
|
if cs.XValueFormatter != nil {
|
||||||
|
x = cs.XValueFormatter
|
||||||
|
} else {
|
||||||
|
x = FloatValueFormatter
|
||||||
|
}
|
||||||
|
if cs.YValueFormatter != nil {
|
||||||
|
y = cs.YValueFormatter
|
||||||
|
} else {
|
||||||
|
y = FloatValueFormatter
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (cs ContinuousSeries) GetYAxis() YAxisType {
|
||||||
|
return cs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := cs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (cs ContinuousSeries) Validate() error {
|
||||||
|
if len(cs.XValues) == 0 {
|
||||||
|
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 nil
|
||||||
|
}
|
72
continuous_series_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContinuousSeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
cs := ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
testutil.AssertEqual(t, "Test Series", cs.GetName())
|
||||||
|
testutil.AssertEqual(t, 10, cs.Len())
|
||||||
|
x0, y0 := cs.GetValues(0)
|
||||||
|
testutil.AssertEqual(t, 1.0, x0)
|
||||||
|
testutil.AssertEqual(t, 1.0, y0)
|
||||||
|
|
||||||
|
xn, yn := cs.GetValues(9)
|
||||||
|
testutil.AssertEqual(t, 10.0, xn)
|
||||||
|
testutil.AssertEqual(t, 10.0, yn)
|
||||||
|
|
||||||
|
xn, yn = cs.GetLastValues()
|
||||||
|
testutil.AssertEqual(t, 10.0, xn)
|
||||||
|
testutil.AssertEqual(t, 10.0, yn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContinuousSeriesValueFormatter(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
cs := ContinuousSeries{
|
||||||
|
XValueFormatter: func(v interface{}) string {
|
||||||
|
return fmt.Sprintf("%f foo", v)
|
||||||
|
},
|
||||||
|
YValueFormatter: func(v interface{}) string {
|
||||||
|
return fmt.Sprintf("%f bar", v)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
xf, yf := cs.GetValueFormatters()
|
||||||
|
testutil.AssertEqual(t, "0.100000 foo", xf(0.1))
|
||||||
|
testutil.AssertEqual(t, "0.100000 bar", yf(0.1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContinuousSeriesValidate(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
cs := ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
testutil.AssertNil(t, cs.Validate())
|
||||||
|
|
||||||
|
cs = ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
XValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
testutil.AssertNotNil(t, cs.Validate())
|
||||||
|
|
||||||
|
cs = ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
YValues: LinearRange(1.0, 10.0),
|
||||||
|
}
|
||||||
|
testutil.AssertNotNil(t, cs.Validate())
|
||||||
|
}
|
103
defaults.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultChartHeight is the default chart height.
|
||||||
|
DefaultChartHeight = 400
|
||||||
|
// DefaultChartWidth is the default chart width.
|
||||||
|
DefaultChartWidth = 1024
|
||||||
|
// DefaultStrokeWidth is the default chart stroke width.
|
||||||
|
DefaultStrokeWidth = 0.0
|
||||||
|
// DefaultDotWidth is the default chart dot width.
|
||||||
|
DefaultDotWidth = 0.0
|
||||||
|
// DefaultSeriesLineWidth is the default line width.
|
||||||
|
DefaultSeriesLineWidth = 1.0
|
||||||
|
// DefaultAxisLineWidth is the line width of the axis lines.
|
||||||
|
DefaultAxisLineWidth = 1.0
|
||||||
|
//DefaultDPI is the default dots per inch for the chart.
|
||||||
|
DefaultDPI = 92.0
|
||||||
|
// DefaultMinimumFontSize is the default minimum font size.
|
||||||
|
DefaultMinimumFontSize = 8.0
|
||||||
|
// DefaultFontSize is the default font size.
|
||||||
|
DefaultFontSize = 10.0
|
||||||
|
// DefaultTitleFontSize is the default title font size.
|
||||||
|
DefaultTitleFontSize = 18.0
|
||||||
|
// DefaultAnnotationDeltaWidth is the width of the left triangle out of annotations.
|
||||||
|
DefaultAnnotationDeltaWidth = 10
|
||||||
|
// DefaultAnnotationFontSize is the font size of annotations.
|
||||||
|
DefaultAnnotationFontSize = 10.0
|
||||||
|
// DefaultAxisFontSize is the font size of the axis labels.
|
||||||
|
DefaultAxisFontSize = 10.0
|
||||||
|
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
||||||
|
DefaultTitleTop = 10
|
||||||
|
|
||||||
|
// DefaultBackgroundStrokeWidth is the default stroke on the chart background.
|
||||||
|
DefaultBackgroundStrokeWidth = 0.0
|
||||||
|
// DefaultCanvasStrokeWidth is the default stroke on the chart canvas.
|
||||||
|
DefaultCanvasStrokeWidth = 0.0
|
||||||
|
|
||||||
|
// DefaultLineSpacing is the default vertical distance between lines of text.
|
||||||
|
DefaultLineSpacing = 5
|
||||||
|
|
||||||
|
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
|
||||||
|
DefaultYAxisMargin = 10
|
||||||
|
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
||||||
|
DefaultXAxisMargin = 10
|
||||||
|
|
||||||
|
//DefaultVerticalTickHeight is half the margin.
|
||||||
|
DefaultVerticalTickHeight = DefaultXAxisMargin >> 1
|
||||||
|
//DefaultHorizontalTickWidth is half the margin.
|
||||||
|
DefaultHorizontalTickWidth = DefaultYAxisMargin >> 1
|
||||||
|
|
||||||
|
// DefaultTickCount is the default number of ticks to show
|
||||||
|
DefaultTickCount = 10
|
||||||
|
// DefaultTickCountSanityCheck is a hard limit on number of ticks to prevent infinite loops.
|
||||||
|
DefaultTickCountSanityCheck = 1 << 10 //1024
|
||||||
|
|
||||||
|
// DefaultMinimumTickHorizontalSpacing is the minimum distance between horizontal ticks.
|
||||||
|
DefaultMinimumTickHorizontalSpacing = 20
|
||||||
|
// DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks.
|
||||||
|
DefaultMinimumTickVerticalSpacing = 20
|
||||||
|
|
||||||
|
// DefaultDateFormat is the default date format.
|
||||||
|
DefaultDateFormat = "2006-01-02"
|
||||||
|
// DefaultDateHourFormat is the date format for hour timestamp formats.
|
||||||
|
DefaultDateHourFormat = "01-02 3PM"
|
||||||
|
// DefaultDateMinuteFormat is the date format for minute range timestamp formats.
|
||||||
|
DefaultDateMinuteFormat = "01-02 3:04PM"
|
||||||
|
// DefaultFloatFormat is the default float format.
|
||||||
|
DefaultFloatFormat = "%.2f"
|
||||||
|
// DefaultPercentValueFormat is the default percent format.
|
||||||
|
DefaultPercentValueFormat = "%0.2f%%"
|
||||||
|
|
||||||
|
// DefaultBarSpacing is the default pixel spacing between bars.
|
||||||
|
DefaultBarSpacing = 100
|
||||||
|
// DefaultBarWidth is the default pixel width of bars in a bar chart.
|
||||||
|
DefaultBarWidth = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DashArrayDots is a dash array that represents '....' style stroke dashes.
|
||||||
|
DashArrayDots = []int{1, 1}
|
||||||
|
// DashArrayDashesSmall is a dash array that represents '- - -' style stroke dashes.
|
||||||
|
DashArrayDashesSmall = []int{3, 3}
|
||||||
|
// DashArrayDashesMedium is a dash array that represents '-- -- --' style stroke dashes.
|
||||||
|
DashArrayDashesMedium = []int{5, 5}
|
||||||
|
// DashArrayDashesLarge is a dash array that represents '----- ----- -----' style stroke dashes.
|
||||||
|
DashArrayDashesLarge = []int{10, 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultAnnotationPadding is the padding around an annotation.
|
||||||
|
DefaultAnnotationPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||||
|
|
||||||
|
// DefaultBackgroundPadding is the default canvas padding config.
|
||||||
|
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContentTypePNG is the png mime type.
|
||||||
|
ContentTypePNG = "image/png"
|
||||||
|
|
||||||
|
// ContentTypeSVG is the svg mime type.
|
||||||
|
ContentTypeSVG = "image/svg+xml"
|
||||||
|
)
|
315
donut_chart.go
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
69
donut_chart_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
325
draw.go
Normal file
|
@ -0,0 +1,325 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Draw contains helpers for drawing common objects.
|
||||||
|
Draw = &draw{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type draw struct{}
|
||||||
|
|
||||||
|
// LineSeries draws a line series with a renderer.
|
||||||
|
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y := vs.GetValues(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y)
|
||||||
|
|
||||||
|
yv0 := yrange.Translate(0)
|
||||||
|
|
||||||
|
var vx, vy float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
if style.ShouldDrawStroke() && style.ShouldDrawFill() {
|
||||||
|
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.LineTo(x, MinInt(cb, cb-yv0))
|
||||||
|
r.LineTo(x0, MinInt(cb, cb-yv0))
|
||||||
|
r.LineTo(x0, y0)
|
||||||
|
r.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.ShouldDrawStroke() {
|
||||||
|
style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.ShouldDrawDot() {
|
||||||
|
defaultDotWidth := style.GetDotWidth()
|
||||||
|
|
||||||
|
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
for i := 0; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
|
||||||
|
dotWidth := defaultDotWidth
|
||||||
|
if style.DotWidthProvider != nil {
|
||||||
|
dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.DotColorProvider != nil {
|
||||||
|
dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
|
||||||
|
|
||||||
|
r.SetFillColor(dotColor)
|
||||||
|
r.SetStrokeColor(dotColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Circle(dotWidth, x, y)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoundedSeries draws a series that implements BoundedValuesProvider.
|
||||||
|
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
|
||||||
|
drawOffsetIndex := 0
|
||||||
|
if len(drawOffsetIndexes) > 0 {
|
||||||
|
drawOffsetIndex = drawOffsetIndexes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y1)
|
||||||
|
|
||||||
|
var vx, vy1, vy2 float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
xvalues := make([]float64, bbs.Len())
|
||||||
|
xvalues[0] = v0x
|
||||||
|
y2values := make([]float64, bbs.Len())
|
||||||
|
y2values[0] = v0y2
|
||||||
|
|
||||||
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < bbs.Len(); i++ {
|
||||||
|
vx, vy1, vy2 = bbs.GetBoundedValues(i)
|
||||||
|
|
||||||
|
xvalues[i] = vx
|
||||||
|
y2values[i] = vy2
|
||||||
|
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy1)
|
||||||
|
if i > drawOffsetIndex {
|
||||||
|
r.LineTo(x, y)
|
||||||
|
} else {
|
||||||
|
r.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
|
||||||
|
vx, vy2 = xvalues[i], y2values[i]
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistogramSeries draws a value provider as boxes from 0.
|
||||||
|
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculate bar width?
|
||||||
|
seriesLength := vs.Len()
|
||||||
|
barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength)))
|
||||||
|
if len(barWidths) > 0 {
|
||||||
|
barWidth = barWidths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
//foreach datapoint, draw a box.
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy := vs.GetValues(index)
|
||||||
|
y0 := yrange.Translate(0)
|
||||||
|
x := cl + xrange.Translate(vx)
|
||||||
|
y := yrange.Translate(vy)
|
||||||
|
|
||||||
|
d.Box(r, Box{
|
||||||
|
Top: cb - y0,
|
||||||
|
Left: x - (barWidth >> 1),
|
||||||
|
Right: x + (barWidth >> 1),
|
||||||
|
Bottom: cb - y,
|
||||||
|
}, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
halfTextHeight := textHeight >> 1
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
strokeWidth := style.GetStrokeWidth()
|
||||||
|
|
||||||
|
top := ly - (pt + halfTextHeight)
|
||||||
|
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
|
||||||
|
bottom := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: top,
|
||||||
|
Left: lx,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation draws an anotation with a renderer.
|
||||||
|
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
textBox := r.MeasureText(label)
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
halfTextHeight := textBox.Height() >> 1
|
||||||
|
|
||||||
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
textX := lx + pl + DefaultAnnotationDeltaWidth
|
||||||
|
textY := ly + halfTextHeight
|
||||||
|
|
||||||
|
ltx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
lbx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(ltx, lty)
|
||||||
|
r.LineTo(rtx, rty)
|
||||||
|
r.LineTo(rbx, rby)
|
||||||
|
r.LineTo(lbx, lby)
|
||||||
|
r.LineTo(lx, ly)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
r.Text(label, textX, textY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box draws a box with a given style.
|
||||||
|
func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
|
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
r.MoveTo(b.Left, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Top)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
||||||
|
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxCorners(r Renderer, bc BoxCorners, 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.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawText draws text with a given style.
|
||||||
|
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
r.Text(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
return r.MeasureText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextWithin draws the text within a given box.
|
||||||
|
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
lines := Text.WrapFit(r, text, box.Width(), style)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, style)
|
||||||
|
|
||||||
|
y := box.Top
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx, ty int
|
||||||
|
for _, line := range lines {
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
switch style.GetTextHorizontalAlign() {
|
||||||
|
case TextHorizontalAlignCenter:
|
||||||
|
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
||||||
|
case TextHorizontalAlignRight:
|
||||||
|
tx = box.Right - lineBox.Width()
|
||||||
|
default:
|
||||||
|
tx = box.Left
|
||||||
|
}
|
||||||
|
if style.TextRotationDegrees == 0 {
|
||||||
|
ty = y + lineBox.Height()
|
||||||
|
} else {
|
||||||
|
ty = y
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Text(line, tx, ty)
|
||||||
|
y += lineBox.Height() + style.GetTextLineSpacing()
|
||||||
|
}
|
||||||
|
}
|
5
drawing/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
go-chart > drawing
|
||||||
|
==================
|
||||||
|
|
||||||
|
The bulk of the code in this package is based on [draw2d](https://github.com/llgcode/draw2d), but
|
||||||
|
with significant modifications to make the APIs more golang friendly and careful about units (points vs. pixels).
|
274
drawing/color.go
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
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}
|
||||||
|
// 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}
|
||||||
|
// 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 {
|
||||||
|
v, _ := strconv.ParseInt(hex, 16, 16)
|
||||||
|
return uint8(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseColor parses a color from a string.
|
||||||
|
func ParseColor(rawColor string) Color {
|
||||||
|
if strings.HasPrefix(rawColor, "rgba") {
|
||||||
|
return ColorFromRGBA(rawColor)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rawColor, "rgb") {
|
||||||
|
return ColorFromRGB(rawColor)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rawColor, "#") {
|
||||||
|
return ColorFromHex(rawColor)
|
||||||
|
}
|
||||||
|
return ColorFromKnown(rawColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgbaexpr = regexp.MustCompile(`rgba\((?P<R>.+),(?P<G>.+),(?P<B>.+),(?P<A>.+)\)`)
|
||||||
|
|
||||||
|
// ColorFromRGBA returns a color from an `rgba()` css function.
|
||||||
|
func ColorFromRGBA(rgba string) (output Color) {
|
||||||
|
values := rgbaexpr.FindStringSubmatch(rgba)
|
||||||
|
for i, name := range rgbaexpr.SubexpNames() {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i >= len(values) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "R":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.R = uint8(parsed)
|
||||||
|
case "G":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.G = uint8(parsed)
|
||||||
|
case "B":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.B = uint8(parsed)
|
||||||
|
case "A":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseFloat(value, 32)
|
||||||
|
if parsed > 1 {
|
||||||
|
parsed = 1
|
||||||
|
} else if parsed < 0 {
|
||||||
|
parsed = 0
|
||||||
|
}
|
||||||
|
output.A = uint8(parsed * 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgbexpr = regexp.MustCompile(`rgb\((?P<R>.+),(?P<G>.+),(?P<B>.+)\)`)
|
||||||
|
|
||||||
|
// ColorFromRGB returns a color from an `rgb()` css function.
|
||||||
|
func ColorFromRGB(rgb string) (output Color) {
|
||||||
|
output.A = 255
|
||||||
|
values := rgbexpr.FindStringSubmatch(rgb)
|
||||||
|
for i, name := range rgbaexpr.SubexpNames() {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i >= len(values) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "R":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.R = uint8(parsed)
|
||||||
|
case "G":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.G = uint8(parsed)
|
||||||
|
case "B":
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||||
|
output.B = uint8(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorFromHex returns a color from a css hex code.
|
||||||
|
//
|
||||||
|
// NOTE: it will trim a leading '#' character if present.
|
||||||
|
func ColorFromHex(hex string) Color {
|
||||||
|
if strings.HasPrefix(hex, "#") {
|
||||||
|
hex = strings.TrimPrefix(hex, "#")
|
||||||
|
}
|
||||||
|
var c Color
|
||||||
|
if len(hex) == 3 {
|
||||||
|
c.R = parseHex(string(hex[0])) * 0x11
|
||||||
|
c.G = parseHex(string(hex[1])) * 0x11
|
||||||
|
c.B = parseHex(string(hex[2])) * 0x11
|
||||||
|
} else {
|
||||||
|
c.R = parseHex(string(hex[0:2]))
|
||||||
|
c.G = parseHex(string(hex[2:4]))
|
||||||
|
c.B = parseHex(string(hex[4:6]))
|
||||||
|
}
|
||||||
|
c.A = 255
|
||||||
|
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
|
||||||
|
var c Color
|
||||||
|
c.R = uint8(float64(r) / fa)
|
||||||
|
c.G = uint8(float64(g) / fa)
|
||||||
|
c.B = uint8(float64(b) / fa)
|
||||||
|
c.A = uint8(a | (a >> 8))
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorChannelFromFloat returns a normalized byte from a given float value.
|
||||||
|
func ColorChannelFromFloat(v float64) uint8 {
|
||||||
|
return uint8(v * 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color is our internal color type because color.Color is bullshit.
|
||||||
|
type Color struct {
|
||||||
|
R, G, B, A uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the color as a pre-alpha mixed color set.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
fa := float64(c.A) / 255.0
|
||||||
|
r = uint32(float64(uint32(c.R)) * fa)
|
||||||
|
r |= r << 8
|
||||||
|
g = uint32(float64(uint32(c.G)) * fa)
|
||||||
|
g |= g << 8
|
||||||
|
b = uint32(float64(uint32(c.B)) * fa)
|
||||||
|
b |= b << 8
|
||||||
|
a = uint32(c.A)
|
||||||
|
a |= a << 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the color has been set or not.
|
||||||
|
func (c Color) IsZero() bool {
|
||||||
|
return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTransparent returns if the colors alpha channel is zero.
|
||||||
|
func (c Color) IsTransparent() bool {
|
||||||
|
return c.A == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAlpha returns a copy of the color with a given alpha.
|
||||||
|
func (c Color) WithAlpha(a uint8) Color {
|
||||||
|
return Color{
|
||||||
|
R: c.R,
|
||||||
|
G: c.G,
|
||||||
|
B: c.B,
|
||||||
|
A: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns true if the color equals another.
|
||||||
|
func (c Color) Equals(other Color) bool {
|
||||||
|
return c.R == other.R &&
|
||||||
|
c.G == other.G &&
|
||||||
|
c.B == other.B &&
|
||||||
|
c.A == other.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// AverageWith averages two colors.
|
||||||
|
func (c Color) AverageWith(other Color) Color {
|
||||||
|
return Color{
|
||||||
|
R: (c.R + other.R) >> 1,
|
||||||
|
G: (c.G + other.G) >> 1,
|
||||||
|
B: (c.B + other.B) >> 1,
|
||||||
|
A: c.A,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a css string representation of the color.
|
||||||
|
func (c Color) String() string {
|
||||||
|
fa := float64(c.A) / float64(255)
|
||||||
|
return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa)
|
||||||
|
}
|
114
drawing/color_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColorFromHex(t *testing.T) {
|
||||||
|
white := ColorFromHex("FFFFFF")
|
||||||
|
testutil.AssertEqual(t, ColorWhite, white)
|
||||||
|
|
||||||
|
shortWhite := ColorFromHex("FFF")
|
||||||
|
testutil.AssertEqual(t, ColorWhite, shortWhite)
|
||||||
|
|
||||||
|
black := ColorFromHex("000000")
|
||||||
|
testutil.AssertEqual(t, ColorBlack, black)
|
||||||
|
|
||||||
|
shortBlack := ColorFromHex("000")
|
||||||
|
testutil.AssertEqual(t, ColorBlack, shortBlack)
|
||||||
|
|
||||||
|
red := ColorFromHex("FF0000")
|
||||||
|
testutil.AssertEqual(t, ColorRed, red)
|
||||||
|
|
||||||
|
shortRed := ColorFromHex("F00")
|
||||||
|
testutil.AssertEqual(t, ColorRed, shortRed)
|
||||||
|
|
||||||
|
green := ColorFromHex("008000")
|
||||||
|
testutil.AssertEqual(t, ColorGreen, green)
|
||||||
|
|
||||||
|
// shortGreen := ColorFromHex("0F0")
|
||||||
|
// testutil.AssertEqual(t, ColorGreen, shortGreen)
|
||||||
|
|
||||||
|
blue := ColorFromHex("0000FF")
|
||||||
|
testutil.AssertEqual(t, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorFromAlphaMixedRGBA(t *testing.T) {
|
||||||
|
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
|
||||||
|
testutil.AssertTrue(t, 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))
|
||||||
|
}
|
||||||
|
}
|
6
drawing/constants.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultDPI is the default image DPI.
|
||||||
|
DefaultDPI = 96.0
|
||||||
|
)
|
185
drawing/curve.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
|
||||||
|
CurveRecursionLimit = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cubic
|
||||||
|
// x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64
|
||||||
|
|
||||||
|
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
|
||||||
|
// c1 and c2 parameters are the resulting curves
|
||||||
|
func SubdivideCubic(c, c1, c2 []float64) {
|
||||||
|
// First point of c is the first point of c1
|
||||||
|
c1[0], c1[1] = c[0], c[1]
|
||||||
|
// Last point of c is the last point of c2
|
||||||
|
c2[6], c2[7] = c[6], c[7]
|
||||||
|
|
||||||
|
// Subdivide segment using midpoints
|
||||||
|
c1[2] = (c[0] + c[2]) / 2
|
||||||
|
c1[3] = (c[1] + c[3]) / 2
|
||||||
|
|
||||||
|
midX := (c[2] + c[4]) / 2
|
||||||
|
midY := (c[3] + c[5]) / 2
|
||||||
|
|
||||||
|
c2[4] = (c[4] + c[6]) / 2
|
||||||
|
c2[5] = (c[5] + c[7]) / 2
|
||||||
|
|
||||||
|
c1[4] = (c1[2] + midX) / 2
|
||||||
|
c1[5] = (c1[3] + midY) / 2
|
||||||
|
|
||||||
|
c2[2] = (midX + c2[4]) / 2
|
||||||
|
c2[3] = (midY + c2[5]) / 2
|
||||||
|
|
||||||
|
c1[6] = (c1[4] + c2[2]) / 2
|
||||||
|
c1[7] = (c1[5] + c2[3]) / 2
|
||||||
|
|
||||||
|
// Last Point of c1 is equal to the first point of c2
|
||||||
|
c2[0], c2[1] = c1[6], c1[7]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceCubic generate lines subdividing the cubic curve using a Liner
|
||||||
|
// flattening_threshold helps determines the flattening expectation of the curve
|
||||||
|
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
||||||
|
// Allocation curves
|
||||||
|
var curves [CurveRecursionLimit * 8]float64
|
||||||
|
copy(curves[0:8], cubic[0:8])
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
// current curve
|
||||||
|
var c []float64
|
||||||
|
|
||||||
|
var dx, dy, d2, d3 float64
|
||||||
|
|
||||||
|
for i >= 0 {
|
||||||
|
c = curves[i*8:]
|
||||||
|
dx = c[6] - c[0]
|
||||||
|
dy = c[7] - c[1]
|
||||||
|
|
||||||
|
d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx)
|
||||||
|
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
|
||||||
|
|
||||||
|
// if it's flat then trace a line
|
||||||
|
if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
||||||
|
t.LineTo(c[6], c[7])
|
||||||
|
i--
|
||||||
|
} else {
|
||||||
|
// second half of bezier go lower onto the stack
|
||||||
|
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quad
|
||||||
|
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||||
|
|
||||||
|
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
|
||||||
|
// c1 and c2 parameters are the resulting curves
|
||||||
|
func SubdivideQuad(c, c1, c2 []float64) {
|
||||||
|
// First point of c is the first point of c1
|
||||||
|
c1[0], c1[1] = c[0], c[1]
|
||||||
|
// Last point of c is the last point of c2
|
||||||
|
c2[4], c2[5] = c[4], c[5]
|
||||||
|
|
||||||
|
// Subdivide segment using midpoints
|
||||||
|
c1[2] = (c[0] + c[2]) / 2
|
||||||
|
c1[3] = (c[1] + c[3]) / 2
|
||||||
|
c2[2] = (c[2] + c[4]) / 2
|
||||||
|
c2[3] = (c[3] + c[5]) / 2
|
||||||
|
c1[4] = (c1[2] + c2[2]) / 2
|
||||||
|
c1[5] = (c1[3] + c2[3]) / 2
|
||||||
|
c2[0], c2[1] = c1[4], c1[5]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceWindowIndices(i int) (startAt, endAt int) {
|
||||||
|
startAt = i * 6
|
||||||
|
endAt = startAt + 6
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceCalcDeltas(c []float64) (dx, dy, d float64) {
|
||||||
|
dx = c[4] - c[0]
|
||||||
|
dy = c[5] - c[1]
|
||||||
|
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceIsFlat(dx, dy, d, threshold float64) bool {
|
||||||
|
return (d * d) < threshold*(dx*dx+dy*dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceGetWindow(curves []float64, i int) []float64 {
|
||||||
|
startAt, endAt := traceWindowIndices(i)
|
||||||
|
return curves[startAt:endAt]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceQuad generate lines subdividing the curve using a Liner
|
||||||
|
// flattening_threshold helps determines the flattening expectation of the curve
|
||||||
|
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||||
|
const curveLen = CurveRecursionLimit * 6
|
||||||
|
const curveEndIndex = curveLen - 1
|
||||||
|
const lastIteration = CurveRecursionLimit - 1
|
||||||
|
|
||||||
|
// Allocates curves stack
|
||||||
|
curves := make([]float64, curveLen)
|
||||||
|
|
||||||
|
// copy 6 elements from the quad path to the stack
|
||||||
|
copy(curves[0:6], quad[0:6])
|
||||||
|
|
||||||
|
var i int
|
||||||
|
var c []float64
|
||||||
|
var dx, dy, d float64
|
||||||
|
|
||||||
|
for i >= 0 {
|
||||||
|
c = traceGetWindow(curves, i)
|
||||||
|
dx, dy, d = traceCalcDeltas(c)
|
||||||
|
|
||||||
|
// bail early if the distance is 0
|
||||||
|
if d == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's flat then trace a line
|
||||||
|
if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
|
||||||
|
t.LineTo(c[4], c[5])
|
||||||
|
i--
|
||||||
|
} else {
|
||||||
|
SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceArc trace an arc using a Liner
|
||||||
|
func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) {
|
||||||
|
end := start + angle
|
||||||
|
clockWise := true
|
||||||
|
if angle < 0 {
|
||||||
|
clockWise = false
|
||||||
|
}
|
||||||
|
ra := (math.Abs(rx) + math.Abs(ry)) / 2
|
||||||
|
da := math.Acos(ra/(ra+0.125/scale)) * 2
|
||||||
|
//normalize
|
||||||
|
if !clockWise {
|
||||||
|
da = -da
|
||||||
|
}
|
||||||
|
angle = start + da
|
||||||
|
var curX, curY float64
|
||||||
|
for {
|
||||||
|
if (angle < end-da/4) != clockWise {
|
||||||
|
curX = x + math.Cos(end)*rx
|
||||||
|
curY = y + math.Sin(end)*ry
|
||||||
|
return curX, curY
|
||||||
|
}
|
||||||
|
curX = x + math.Cos(angle)*rx
|
||||||
|
curY = y + math.Sin(angle)*ry
|
||||||
|
|
||||||
|
angle += da
|
||||||
|
t.LineTo(curX, curY)
|
||||||
|
}
|
||||||
|
}
|
35
drawing/curve_test.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type point struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockLine struct {
|
||||||
|
inner []point
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockLine) LineTo(x, y float64) {
|
||||||
|
ml.inner = append(ml.inner, point{x, y})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml mockLine) Len() int {
|
||||||
|
return len(ml.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraceQuad(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
// Quad
|
||||||
|
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||||
|
// do the 9->12 circle segment
|
||||||
|
quad := []float64{10, 20, 20, 20, 20, 10}
|
||||||
|
liner := &mockLine{}
|
||||||
|
TraceQuad(liner, quad, 0.5)
|
||||||
|
testutil.AssertNotZero(t, liner.Len())
|
||||||
|
}
|
89
drawing/dasher.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// NewDashVertexConverter creates a new dash converter.
|
||||||
|
func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter {
|
||||||
|
var dasher DashVertexConverter
|
||||||
|
dasher.dash = dash
|
||||||
|
dasher.currentDash = 0
|
||||||
|
dasher.dashOffset = dashOffset
|
||||||
|
dasher.next = flattener
|
||||||
|
return &dasher
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashVertexConverter is a converter for dash vertexes.
|
||||||
|
type DashVertexConverter struct {
|
||||||
|
next Flattener
|
||||||
|
x, y, distance float64
|
||||||
|
dash []float64
|
||||||
|
currentDash int
|
||||||
|
dashOffset float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) LineTo(x, y float64) {
|
||||||
|
dasher.lineTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) MoveTo(x, y float64) {
|
||||||
|
dasher.next.MoveTo(x, y)
|
||||||
|
dasher.x, dasher.y = x, y
|
||||||
|
dasher.distance = dasher.dashOffset
|
||||||
|
dasher.currentDash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) LineJoin() {
|
||||||
|
dasher.next.LineJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) Close() {
|
||||||
|
dasher.next.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) End() {
|
||||||
|
dasher.next.End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dasher *DashVertexConverter) lineTo(x, y float64) {
|
||||||
|
rest := dasher.dash[dasher.currentDash] - dasher.distance
|
||||||
|
for rest < 0 {
|
||||||
|
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
rest = dasher.dash[dasher.currentDash] - dasher.distance
|
||||||
|
}
|
||||||
|
d := distance(dasher.x, dasher.y, x, y)
|
||||||
|
for d >= rest {
|
||||||
|
k := rest / d
|
||||||
|
lx := dasher.x + k*(x-dasher.x)
|
||||||
|
ly := dasher.y + k*(y-dasher.y)
|
||||||
|
if dasher.currentDash%2 == 0 {
|
||||||
|
// line
|
||||||
|
dasher.next.LineTo(lx, ly)
|
||||||
|
} else {
|
||||||
|
// gap
|
||||||
|
dasher.next.End()
|
||||||
|
dasher.next.MoveTo(lx, ly)
|
||||||
|
}
|
||||||
|
d = d - rest
|
||||||
|
dasher.x, dasher.y = lx, ly
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
rest = dasher.dash[dasher.currentDash]
|
||||||
|
}
|
||||||
|
dasher.distance = d
|
||||||
|
if dasher.currentDash%2 == 0 {
|
||||||
|
// line
|
||||||
|
dasher.next.LineTo(x, y)
|
||||||
|
} else {
|
||||||
|
// gap
|
||||||
|
dasher.next.End()
|
||||||
|
dasher.next.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
if dasher.distance >= dasher.dash[dasher.currentDash] {
|
||||||
|
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
}
|
||||||
|
dasher.x, dasher.y = x, y
|
||||||
|
}
|
41
drawing/demux_flattener.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// DemuxFlattener is a flattener
|
||||||
|
type DemuxFlattener struct {
|
||||||
|
Flatteners []Flattener
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) MoveTo(x, y float64) {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) LineTo(x, y float64) {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) LineJoin() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.LineJoin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) Close() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) End() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
}
|
148
drawing/drawing.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FillRule defines the type for fill rules
|
||||||
|
type FillRule int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FillRuleEvenOdd determines the "insideness" of a point in the shape
|
||||||
|
// by drawing a ray from that point to infinity in any direction
|
||||||
|
// and counting the number of path segments from the given shape that the ray crosses.
|
||||||
|
// If this number is odd, the point is inside; if even, the point is outside.
|
||||||
|
FillRuleEvenOdd FillRule = iota
|
||||||
|
// FillRuleWinding determines the "insideness" of a point in the shape
|
||||||
|
// by drawing a ray from that point to infinity in any direction
|
||||||
|
// and then examining the places where a segment of the shape crosses the ray.
|
||||||
|
// Starting with a count of zero, add one each time a path segment crosses
|
||||||
|
// the ray from left to right and subtract one each time
|
||||||
|
// a path segment crosses the ray from right to left. After counting the crossings,
|
||||||
|
// if the result is zero then the point is outside the path. Otherwise, it is inside.
|
||||||
|
FillRuleWinding
|
||||||
|
)
|
||||||
|
|
||||||
|
// LineCap is the style of line extremities
|
||||||
|
type LineCap int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RoundCap defines a rounded shape at the end of the line
|
||||||
|
RoundCap LineCap = iota
|
||||||
|
// ButtCap defines a squared shape exactly at the end of the line
|
||||||
|
ButtCap
|
||||||
|
// SquareCap defines a squared shape at the end of the line
|
||||||
|
SquareCap
|
||||||
|
)
|
||||||
|
|
||||||
|
// LineJoin is the style of segments joint
|
||||||
|
type LineJoin int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BevelJoin represents cut segments joint
|
||||||
|
BevelJoin LineJoin = iota
|
||||||
|
// RoundJoin represents rounded segments joint
|
||||||
|
RoundJoin
|
||||||
|
// MiterJoin represents peaker segments joint
|
||||||
|
MiterJoin
|
||||||
|
)
|
||||||
|
|
||||||
|
// StrokeStyle keeps stroke style attributes
|
||||||
|
// that is used by the Stroke method of a Drawer
|
||||||
|
type StrokeStyle struct {
|
||||||
|
// Color defines the color of stroke
|
||||||
|
Color color.Color
|
||||||
|
// Line width
|
||||||
|
Width float64
|
||||||
|
// Line cap style rounded, butt or square
|
||||||
|
LineCap LineCap
|
||||||
|
// Line join style bevel, round or miter
|
||||||
|
LineJoin LineJoin
|
||||||
|
// offset of the first dash
|
||||||
|
DashOffset float64
|
||||||
|
// array represented dash length pair values are plain dash and impair are space between dash
|
||||||
|
// if empty display plain line
|
||||||
|
Dash []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// SolidFillStyle define style attributes for a solid fill style
|
||||||
|
type SolidFillStyle struct {
|
||||||
|
// Color defines the line color
|
||||||
|
Color color.Color
|
||||||
|
// FillRule defines the file rule to used
|
||||||
|
FillRule FillRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valign Vertical Alignment of the text
|
||||||
|
type Valign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ValignTop top align text
|
||||||
|
ValignTop Valign = iota
|
||||||
|
// ValignCenter centered text
|
||||||
|
ValignCenter
|
||||||
|
// ValignBottom bottom aligned text
|
||||||
|
ValignBottom
|
||||||
|
// ValignBaseline align text with the baseline of the font
|
||||||
|
ValignBaseline
|
||||||
|
)
|
||||||
|
|
||||||
|
// Halign Horizontal Alignment of the text
|
||||||
|
type Halign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HalignLeft Horizontally align to left
|
||||||
|
HalignLeft = iota
|
||||||
|
// HalignCenter Horizontally align to center
|
||||||
|
HalignCenter
|
||||||
|
// HalignRight Horizontally align to right
|
||||||
|
HalignRight
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextStyle describe text property
|
||||||
|
type TextStyle struct {
|
||||||
|
// Color defines the color of text
|
||||||
|
Color color.Color
|
||||||
|
// Size font size
|
||||||
|
Size float64
|
||||||
|
// The font to use
|
||||||
|
Font *truetype.Font
|
||||||
|
// Horizontal Alignment of the text
|
||||||
|
Halign Halign
|
||||||
|
// Vertical Alignment of the text
|
||||||
|
Valign Valign
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalingPolicy is a constant to define how to scale an image
|
||||||
|
type ScalingPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ScalingNone no scaling applied
|
||||||
|
ScalingNone ScalingPolicy = iota
|
||||||
|
// ScalingStretch the image is stretched so that its width and height are exactly the given width and height
|
||||||
|
ScalingStretch
|
||||||
|
// ScalingWidth the image is scaled so that its width is exactly the given width
|
||||||
|
ScalingWidth
|
||||||
|
// ScalingHeight the image is scaled so that its height is exactly the given height
|
||||||
|
ScalingHeight
|
||||||
|
// ScalingFit the image is scaled to the largest scale that allow the image to fit within a rectangle width x height
|
||||||
|
ScalingFit
|
||||||
|
// ScalingSameArea the image is scaled so that its area is exactly the area of the given rectangle width x height
|
||||||
|
ScalingSameArea
|
||||||
|
// ScalingFill the image is scaled to the smallest scale that allow the image to fully cover a rectangle width x height
|
||||||
|
ScalingFill
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageScaling style attributes used to display the image
|
||||||
|
type ImageScaling struct {
|
||||||
|
// Horizontal Alignment of the image
|
||||||
|
Halign Halign
|
||||||
|
// Vertical Alignment of the image
|
||||||
|
Valign Valign
|
||||||
|
// Width Height used by scaling policy
|
||||||
|
Width, Height float64
|
||||||
|
// ScalingPolicy defines the scaling policy to applied to the image
|
||||||
|
ScalingPolicy ScalingPolicy
|
||||||
|
}
|
97
drawing/flattener.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// Liner receive segment definition
|
||||||
|
type Liner interface {
|
||||||
|
// LineTo Draw a line from the current position to the point (x, y)
|
||||||
|
LineTo(x, y float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flattener receive segment definition
|
||||||
|
type Flattener interface {
|
||||||
|
// MoveTo Start a New line from the point (x, y)
|
||||||
|
MoveTo(x, y float64)
|
||||||
|
// LineTo Draw a line from the current position to the point (x, y)
|
||||||
|
LineTo(x, y float64)
|
||||||
|
// LineJoin add the most recent starting point to close the path to create a polygon
|
||||||
|
LineJoin()
|
||||||
|
// Close add the most recent starting point to close the path to create a polygon
|
||||||
|
Close()
|
||||||
|
// End mark the current line as finished so we can draw caps
|
||||||
|
End()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten convert curves into straight segments keeping join segments info
|
||||||
|
func Flatten(path *Path, flattener Flattener, scale float64) {
|
||||||
|
// First Point
|
||||||
|
var startX, startY float64
|
||||||
|
// Current Point
|
||||||
|
var x, y float64
|
||||||
|
var i int
|
||||||
|
for _, cmp := range path.Components {
|
||||||
|
switch cmp {
|
||||||
|
case MoveToComponent:
|
||||||
|
x, y = path.Points[i], path.Points[i+1]
|
||||||
|
startX, startY = x, y
|
||||||
|
if i != 0 {
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
flattener.MoveTo(x, y)
|
||||||
|
i += 2
|
||||||
|
case LineToComponent:
|
||||||
|
x, y = path.Points[i], path.Points[i+1]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
flattener.LineJoin()
|
||||||
|
i += 2
|
||||||
|
case QuadCurveToComponent:
|
||||||
|
// we include the previous point for the start of the curve
|
||||||
|
TraceQuad(flattener, path.Points[i-2:], 0.5)
|
||||||
|
x, y = path.Points[i+2], path.Points[i+3]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 4
|
||||||
|
case CubicCurveToComponent:
|
||||||
|
TraceCubic(flattener, path.Points[i-2:], 0.5)
|
||||||
|
x, y = path.Points[i+4], path.Points[i+5]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 6
|
||||||
|
case ArcToComponent:
|
||||||
|
x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale)
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 6
|
||||||
|
case CloseComponent:
|
||||||
|
flattener.LineTo(startX, startY)
|
||||||
|
flattener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SegmentedPath is a path of disparate point sectinos.
|
||||||
|
type SegmentedPath struct {
|
||||||
|
Points []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path interface.
|
||||||
|
func (p *SegmentedPath) MoveTo(x, y float64) {
|
||||||
|
p.Points = append(p.Points, x, y)
|
||||||
|
// TODO need to mark this point as moveto
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path interface.
|
||||||
|
func (p *SegmentedPath) LineTo(x, y float64) {
|
||||||
|
p.Points = append(p.Points, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path interface.
|
||||||
|
func (p *SegmentedPath) LineJoin() {
|
||||||
|
// TODO need to mark the current point as linejoin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path interface.
|
||||||
|
func (p *SegmentedPath) Close() {
|
||||||
|
// TODO Close
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path interface.
|
||||||
|
func (p *SegmentedPath) End() {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
30
drawing/free_type_path.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FtLineBuilder is a builder for freetype raster glyphs.
|
||||||
|
type FtLineBuilder struct {
|
||||||
|
Adder raster.Adder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) MoveTo(x, y float64) {
|
||||||
|
liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) LineTo(x, y float64) {
|
||||||
|
liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) LineJoin() {}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) Close() {}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) End() {}
|
82
drawing/graphic_context.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
||||||
|
type GraphicContext interface {
|
||||||
|
// PathBuilder describes the interface for path drawing
|
||||||
|
PathBuilder
|
||||||
|
// BeginPath creates a new path
|
||||||
|
BeginPath()
|
||||||
|
// GetMatrixTransform returns the current transformation matrix
|
||||||
|
GetMatrixTransform() Matrix
|
||||||
|
// SetMatrixTransform sets the current transformation matrix
|
||||||
|
SetMatrixTransform(tr Matrix)
|
||||||
|
// ComposeMatrixTransform composes the current transformation matrix with tr
|
||||||
|
ComposeMatrixTransform(tr Matrix)
|
||||||
|
// Rotate applies a rotation to the current transformation matrix. angle is in radian.
|
||||||
|
Rotate(angle float64)
|
||||||
|
// Translate applies a translation to the current transformation matrix.
|
||||||
|
Translate(tx, ty float64)
|
||||||
|
// Scale applies a scale to the current transformation matrix.
|
||||||
|
Scale(sx, sy float64)
|
||||||
|
// SetStrokeColor sets the current stroke color
|
||||||
|
SetStrokeColor(c color.Color)
|
||||||
|
// SetFillColor sets the current fill color
|
||||||
|
SetFillColor(c color.Color)
|
||||||
|
// SetFillRule sets the current fill rule
|
||||||
|
SetFillRule(f FillRule)
|
||||||
|
// SetLineWidth sets the current line width
|
||||||
|
SetLineWidth(lineWidth float64)
|
||||||
|
// SetLineCap sets the current line cap
|
||||||
|
SetLineCap(cap LineCap)
|
||||||
|
// SetLineJoin sets the current line join
|
||||||
|
SetLineJoin(join LineJoin)
|
||||||
|
// SetLineDash sets the current dash
|
||||||
|
SetLineDash(dash []float64, dashOffset float64)
|
||||||
|
// SetFontSize sets the current font size
|
||||||
|
SetFontSize(fontSize float64)
|
||||||
|
// GetFontSize gets the current font size
|
||||||
|
GetFontSize() float64
|
||||||
|
// SetFont sets the font for the context
|
||||||
|
SetFont(f *truetype.Font)
|
||||||
|
// GetFont returns the current font
|
||||||
|
GetFont() *truetype.Font
|
||||||
|
// DrawImage draws the raster image in the current canvas
|
||||||
|
DrawImage(image image.Image)
|
||||||
|
// Save the context and push it to the context stack
|
||||||
|
Save()
|
||||||
|
// Restore remove the current context and restore the last one
|
||||||
|
Restore()
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
|
Clear()
|
||||||
|
// ClearRect fills the specified rectangle with a default transparent color
|
||||||
|
ClearRect(x1, y1, x2, y2 int)
|
||||||
|
// SetDPI sets the current DPI
|
||||||
|
SetDPI(dpi int)
|
||||||
|
// GetDPI gets the current DPI
|
||||||
|
GetDPI() int
|
||||||
|
// GetStringBounds gets pixel bounds(dimensions) of given string
|
||||||
|
GetStringBounds(s string) (left, top, right, bottom float64)
|
||||||
|
// CreateStringPath creates a path from the string s at x, y
|
||||||
|
CreateStringPath(text string, x, y float64) (cursor float64)
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
|
FillString(text string) (cursor float64)
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
|
FillStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
|
StrokeString(text string) (cursor float64)
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
|
StrokeStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
|
Stroke(paths ...*Path)
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
|
Fill(paths ...*Path)
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
|
FillStroke(paths ...*Path)
|
||||||
|
}
|
13
drawing/image_filter.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// ImageFilter defines the type of filter to use
|
||||||
|
type ImageFilter int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LinearFilter defines a linear filter
|
||||||
|
LinearFilter ImageFilter = iota
|
||||||
|
// BilinearFilter defines a bilinear filter
|
||||||
|
BilinearFilter
|
||||||
|
// BicubicFilter defines a bicubic filter
|
||||||
|
BicubicFilter
|
||||||
|
)
|
48
drawing/line.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolylineBresenham draws a polyline to an image
|
||||||
|
func PolylineBresenham(img draw.Image, c color.Color, s ...float64) {
|
||||||
|
for i := 2; i < len(s); i += 2 {
|
||||||
|
Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham draws a line between (x0, y0) and (x1, y1)
|
||||||
|
func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) {
|
||||||
|
dx := abs(x1 - x0)
|
||||||
|
dy := abs(y1 - y0)
|
||||||
|
var sx, sy int
|
||||||
|
if x0 < x1 {
|
||||||
|
sx = 1
|
||||||
|
} else {
|
||||||
|
sx = -1
|
||||||
|
}
|
||||||
|
if y0 < y1 {
|
||||||
|
sy = 1
|
||||||
|
} else {
|
||||||
|
sy = -1
|
||||||
|
}
|
||||||
|
err := dx - dy
|
||||||
|
|
||||||
|
var e2 int
|
||||||
|
for {
|
||||||
|
img.Set(x0, y0, color)
|
||||||
|
if x0 == x1 && y0 == y1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e2 = 2 * err
|
||||||
|
if e2 > -dy {
|
||||||
|
err = err - dy
|
||||||
|
x0 = x0 + sx
|
||||||
|
}
|
||||||
|
if e2 < dx {
|
||||||
|
err = err + dx
|
||||||
|
y0 = y0 + sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
220
drawing/matrix.go
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matrix represents an affine transformation
|
||||||
|
type Matrix [6]float64
|
||||||
|
|
||||||
|
const (
|
||||||
|
epsilon = 1e-6
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determinant compute the determinant of the matrix
|
||||||
|
func (tr Matrix) Determinant() float64 {
|
||||||
|
return tr[0]*tr[3] - tr[1]*tr[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform applies the transformation matrix to points. It modify the points passed in parameter.
|
||||||
|
func (tr Matrix) Transform(points []float64) {
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = x*tr[0] + y*tr[2] + tr[4]
|
||||||
|
points[j] = x*tr[1] + y*tr[3] + tr[5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformPoint applies the transformation matrix to point. It returns the point the transformed point.
|
||||||
|
func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) {
|
||||||
|
xres = x*tr[0] + y*tr[2] + tr[4]
|
||||||
|
yres = x*tr[1] + y*tr[3] + tr[5]
|
||||||
|
return xres, yres
|
||||||
|
}
|
||||||
|
|
||||||
|
func minMax(x, y float64) (min, max float64) {
|
||||||
|
if x > y {
|
||||||
|
return y, x
|
||||||
|
}
|
||||||
|
return x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformRectangle applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle
|
||||||
|
func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) {
|
||||||
|
points := []float64{x0, y0, x2, y0, x2, y2, x0, y2}
|
||||||
|
tr.Transform(points)
|
||||||
|
points[0], points[2] = minMax(points[0], points[2])
|
||||||
|
points[4], points[6] = minMax(points[4], points[6])
|
||||||
|
points[1], points[3] = minMax(points[1], points[3])
|
||||||
|
points[5], points[7] = minMax(points[5], points[7])
|
||||||
|
|
||||||
|
nx0 = math.Min(points[0], points[4])
|
||||||
|
ny0 = math.Min(points[1], points[5])
|
||||||
|
nx2 = math.Max(points[2], points[6])
|
||||||
|
ny2 = math.Max(points[3], points[7])
|
||||||
|
return nx0, ny0, nx2, ny2
|
||||||
|
}
|
||||||
|
|
||||||
|
// InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle
|
||||||
|
func (tr Matrix) InverseTransform(points []float64) {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
|
||||||
|
points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point.
|
||||||
|
func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
|
||||||
|
yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
|
||||||
|
return xres, yres
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix.
|
||||||
|
// It modify the points passed in parameter.
|
||||||
|
func (tr Matrix) VectorTransform(points []float64) {
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = x*tr[0] + y*tr[2]
|
||||||
|
points[j] = x*tr[1] + y*tr[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIdentityMatrix creates an identity transformation matrix.
|
||||||
|
func NewIdentityMatrix() Matrix {
|
||||||
|
return Matrix{1, 0, 0, 1, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter
|
||||||
|
func NewTranslationMatrix(tx, ty float64) Matrix {
|
||||||
|
return Matrix{1, 0, 0, 1, tx, ty}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScaleMatrix creates a transformation matrix with a sx, sy scale factor
|
||||||
|
func NewScaleMatrix(sx, sy float64) Matrix {
|
||||||
|
return Matrix{sx, 0, 0, sy, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRotationMatrix creates a rotation transformation matrix. angle is in radian
|
||||||
|
func NewRotationMatrix(angle float64) Matrix {
|
||||||
|
c := math.Cos(angle)
|
||||||
|
s := math.Sin(angle)
|
||||||
|
return Matrix{c, s, -s, c, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2.
|
||||||
|
func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix {
|
||||||
|
xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0])
|
||||||
|
yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1])
|
||||||
|
xOffset := rectangle2[0] - (rectangle1[0] * xScale)
|
||||||
|
yOffset := rectangle2[1] - (rectangle1[1] * yScale)
|
||||||
|
return Matrix{xScale, 0, 0, yScale, xOffset, yOffset}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse computes the inverse matrix
|
||||||
|
func (tr *Matrix) Inverse() {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
|
||||||
|
tr[0] = tr3 / d
|
||||||
|
tr[1] = -tr1 / d
|
||||||
|
tr[2] = -tr2 / d
|
||||||
|
tr[3] = tr0 / d
|
||||||
|
tr[4] = (tr2*tr5 - tr3*tr4) / d
|
||||||
|
tr[5] = (tr1*tr4 - tr0*tr5) / d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the matrix.
|
||||||
|
func (tr Matrix) Copy() Matrix {
|
||||||
|
var result Matrix
|
||||||
|
copy(result[:], tr[:])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose multiplies trToConcat x tr
|
||||||
|
func (tr *Matrix) Compose(trToCompose Matrix) {
|
||||||
|
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
|
||||||
|
tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2
|
||||||
|
tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1
|
||||||
|
tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2
|
||||||
|
tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1
|
||||||
|
tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4
|
||||||
|
tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale adds a scale to the matrix
|
||||||
|
func (tr *Matrix) Scale(sx, sy float64) {
|
||||||
|
tr[0] = sx * tr[0]
|
||||||
|
tr[1] = sx * tr[1]
|
||||||
|
tr[2] = sy * tr[2]
|
||||||
|
tr[3] = sy * tr[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate adds a translation to the matrix
|
||||||
|
func (tr *Matrix) Translate(tx, ty float64) {
|
||||||
|
tr[4] = tx*tr[0] + ty*tr[2] + tr[4]
|
||||||
|
tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate adds a rotation to the matrix.
|
||||||
|
func (tr *Matrix) Rotate(radians float64) {
|
||||||
|
c := math.Cos(radians)
|
||||||
|
s := math.Sin(radians)
|
||||||
|
t0 := c*tr[0] + s*tr[2]
|
||||||
|
t1 := s*tr[3] + c*tr[1]
|
||||||
|
t2 := c*tr[2] - s*tr[0]
|
||||||
|
t3 := c*tr[3] - s*tr[1]
|
||||||
|
tr[0] = t0
|
||||||
|
tr[1] = t1
|
||||||
|
tr[2] = t2
|
||||||
|
tr[3] = t3
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTranslation gets the matrix traslation.
|
||||||
|
func (tr Matrix) GetTranslation() (x, y float64) {
|
||||||
|
return tr[4], tr[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScaling gets the matrix scaling.
|
||||||
|
func (tr Matrix) GetScaling() (x, y float64) {
|
||||||
|
return tr[0], tr[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScale computes a scale for the matrix
|
||||||
|
func (tr Matrix) GetScale() float64 {
|
||||||
|
x := 0.707106781*tr[0] + 0.707106781*tr[1]
|
||||||
|
y := 0.707106781*tr[2] + 0.707106781*tr[3]
|
||||||
|
return math.Sqrt(x*x + y*y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ******************** Testing ********************
|
||||||
|
|
||||||
|
// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) Equals(tr2 Matrix) bool {
|
||||||
|
for i := 0; i < 6; i = i + 1 {
|
||||||
|
if !fequals(tr[i], tr2[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) IsIdentity() bool {
|
||||||
|
return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) IsTranslation() bool {
|
||||||
|
return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise
|
||||||
|
func fequals(float1, float2 float64) bool {
|
||||||
|
return math.Abs(float1-float2) <= epsilon
|
||||||
|
}
|
31
drawing/painter.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/math/f64"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter
|
||||||
|
type Painter interface {
|
||||||
|
raster.Painter
|
||||||
|
SetColor(color color.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawImage draws an image into dest using an affine transformation matrix, an op and a filter
|
||||||
|
func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) {
|
||||||
|
var transformer draw.Transformer
|
||||||
|
switch filter {
|
||||||
|
case LinearFilter:
|
||||||
|
transformer = draw.NearestNeighbor
|
||||||
|
case BilinearFilter:
|
||||||
|
transformer = draw.BiLinear
|
||||||
|
case BicubicFilter:
|
||||||
|
transformer = draw.CatmullRom
|
||||||
|
}
|
||||||
|
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
|
||||||
|
}
|
186
drawing/path.go
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PathBuilder describes the interface for path drawing.
|
||||||
|
type PathBuilder interface {
|
||||||
|
// LastPoint returns the current point of the current sub path
|
||||||
|
LastPoint() (x, y float64)
|
||||||
|
// MoveTo creates a new subpath that start at the specified point
|
||||||
|
MoveTo(x, y float64)
|
||||||
|
// LineTo adds a line to the current subpath
|
||||||
|
LineTo(x, y float64)
|
||||||
|
// QuadCurveTo adds a quadratic Bézier curve to the current subpath
|
||||||
|
QuadCurveTo(cx, cy, x, y float64)
|
||||||
|
// CubicCurveTo adds a cubic Bézier curve to the current subpath
|
||||||
|
CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64)
|
||||||
|
// ArcTo adds an arc to the current subpath
|
||||||
|
ArcTo(cx, cy, rx, ry, startAngle, angle float64)
|
||||||
|
// Close creates a line from the current point to the last MoveTo
|
||||||
|
// point (if not the same) and mark the path as closed so the
|
||||||
|
// first and last lines join nicely.
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathComponent represents component of a path
|
||||||
|
type PathComponent int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MoveToComponent is a MoveTo component in a Path
|
||||||
|
MoveToComponent PathComponent = iota
|
||||||
|
// LineToComponent is a LineTo component in a Path
|
||||||
|
LineToComponent
|
||||||
|
// QuadCurveToComponent is a QuadCurveTo component in a Path
|
||||||
|
QuadCurveToComponent
|
||||||
|
// CubicCurveToComponent is a CubicCurveTo component in a Path
|
||||||
|
CubicCurveToComponent
|
||||||
|
// ArcToComponent is a ArcTo component in a Path
|
||||||
|
ArcToComponent
|
||||||
|
// CloseComponent is a ArcTo component in a Path
|
||||||
|
CloseComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path stores points
|
||||||
|
type Path struct {
|
||||||
|
// Components is a slice of PathComponent in a Path and mark the role of each points in the Path
|
||||||
|
Components []PathComponent
|
||||||
|
// Points are combined with Components to have a specific role in the path
|
||||||
|
Points []float64
|
||||||
|
// Last Point of the Path
|
||||||
|
x, y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Path) appendToPath(cmd PathComponent, points ...float64) {
|
||||||
|
p.Components = append(p.Components, cmd)
|
||||||
|
p.Points = append(p.Points, points...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastPoint returns the current point of the current path
|
||||||
|
func (p *Path) LastPoint() (x, y float64) {
|
||||||
|
return p.x, p.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo starts a new path at (x, y) position
|
||||||
|
func (p *Path) MoveTo(x, y float64) {
|
||||||
|
p.appendToPath(MoveToComponent, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo adds a line to the current path
|
||||||
|
func (p *Path) LineTo(x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(LineToComponent, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo adds a quadratic bezier curve to the current path
|
||||||
|
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(QuadCurveToComponent, cx, cy, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubicCurveTo adds a cubic bezier curve to the current path
|
||||||
|
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(CubicCurveToComponent, cx1, cy1, cx2, cy2, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo adds an arc to the path
|
||||||
|
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
|
endAngle := startAngle + delta
|
||||||
|
clockWise := true
|
||||||
|
if delta < 0 {
|
||||||
|
clockWise = false
|
||||||
|
}
|
||||||
|
// normalize
|
||||||
|
if clockWise {
|
||||||
|
for endAngle < startAngle {
|
||||||
|
endAngle += math.Pi * 2.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for startAngle < endAngle {
|
||||||
|
startAngle += math.Pi * 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startX := cx + math.Cos(startAngle)*rx
|
||||||
|
startY := cy + math.Sin(startAngle)*ry
|
||||||
|
if len(p.Components) > 0 {
|
||||||
|
p.LineTo(startX, startY)
|
||||||
|
} else {
|
||||||
|
p.MoveTo(startX, startY)
|
||||||
|
}
|
||||||
|
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, delta)
|
||||||
|
p.x = cx + math.Cos(endAngle)*rx
|
||||||
|
p.y = cy + math.Sin(endAngle)*ry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the current path
|
||||||
|
func (p *Path) Close() {
|
||||||
|
p.appendToPath(CloseComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy make a clone of the current path and return it
|
||||||
|
func (p *Path) Copy() (dest *Path) {
|
||||||
|
dest = new(Path)
|
||||||
|
dest.Components = make([]PathComponent, len(p.Components))
|
||||||
|
copy(dest.Components, p.Components)
|
||||||
|
dest.Points = make([]float64, len(p.Points))
|
||||||
|
copy(dest.Points, p.Points)
|
||||||
|
dest.x, dest.y = p.x, p.y
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear reset the path
|
||||||
|
func (p *Path) Clear() {
|
||||||
|
p.Components = p.Components[0:0]
|
||||||
|
p.Points = p.Points[0:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the path is empty
|
||||||
|
func (p *Path) IsEmpty() bool {
|
||||||
|
return len(p.Components) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a debug text view of the path
|
||||||
|
func (p *Path) String() string {
|
||||||
|
s := ""
|
||||||
|
j := 0
|
||||||
|
for _, cmd := range p.Components {
|
||||||
|
switch cmd {
|
||||||
|
case MoveToComponent:
|
||||||
|
s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1])
|
||||||
|
j = j + 2
|
||||||
|
case LineToComponent:
|
||||||
|
s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1])
|
||||||
|
j = j + 2
|
||||||
|
case QuadCurveToComponent:
|
||||||
|
s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3])
|
||||||
|
j = j + 4
|
||||||
|
case CubicCurveToComponent:
|
||||||
|
s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
|
||||||
|
j = j + 6
|
||||||
|
case ArcToComponent:
|
||||||
|
s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
|
||||||
|
j = j + 6
|
||||||
|
case CloseComponent:
|
||||||
|
s += "Close\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
283
drawing/raster_graphic_context.go
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRasterGraphicContext creates a new Graphic context from an image.
|
||||||
|
func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) {
|
||||||
|
var painter Painter
|
||||||
|
switch selectImage := img.(type) {
|
||||||
|
case *image.RGBA:
|
||||||
|
painter = raster.NewRGBAPainter(selectImage)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("NewRasterGraphicContext() :: invalid image type")
|
||||||
|
}
|
||||||
|
return NewRasterGraphicContextWithPainter(img, painter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go)
|
||||||
|
func NewRasterGraphicContextWithPainter(img draw.Image, painter Painter) *RasterGraphicContext {
|
||||||
|
width, height := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
return &RasterGraphicContext{
|
||||||
|
NewStackGraphicContext(),
|
||||||
|
img,
|
||||||
|
painter,
|
||||||
|
raster.NewRasterizer(width, height),
|
||||||
|
raster.NewRasterizer(width, height),
|
||||||
|
&truetype.GlyphBuf{},
|
||||||
|
DefaultDPI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RasterGraphicContext is the implementation of GraphicContext for a raster image
|
||||||
|
type RasterGraphicContext struct {
|
||||||
|
*StackGraphicContext
|
||||||
|
img draw.Image
|
||||||
|
painter Painter
|
||||||
|
fillRasterizer *raster.Rasterizer
|
||||||
|
strokeRasterizer *raster.Rasterizer
|
||||||
|
glyphBuf *truetype.GlyphBuf
|
||||||
|
DPI float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDPI sets the screen resolution in dots per inch.
|
||||||
|
func (rgc *RasterGraphicContext) SetDPI(dpi float64) {
|
||||||
|
rgc.DPI = dpi
|
||||||
|
rgc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the resolution of the Image GraphicContext
|
||||||
|
func (rgc *RasterGraphicContext) GetDPI() float64 {
|
||||||
|
return rgc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
|
func (rgc *RasterGraphicContext) Clear() {
|
||||||
|
width, height := rgc.img.Bounds().Dx(), rgc.img.Bounds().Dy()
|
||||||
|
rgc.ClearRect(0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRect fills the current canvas with a default transparent color at the specified rectangle
|
||||||
|
func (rgc *RasterGraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
||||||
|
imageColor := image.NewUniform(rgc.current.FillColor)
|
||||||
|
draw.Draw(rgc.img, image.Rect(x1, y1, x2, y2), imageColor, image.ZP, draw.Over)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawImage draws the raster image in the current canvas
|
||||||
|
func (rgc *RasterGraphicContext) DrawImage(img image.Image) {
|
||||||
|
DrawImage(img, rgc.img, rgc.current.Tr, draw.Over, BilinearFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
|
func (rgc *RasterGraphicContext) FillString(text string) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.FillStringAt(text, 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
|
func (rgc *RasterGraphicContext) FillStringAt(text string, x, y float64) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.CreateStringPath(text, x, y)
|
||||||
|
rgc.Fill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
|
func (rgc *RasterGraphicContext) StrokeString(text string) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.StrokeStringAt(text, 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
|
func (rgc *RasterGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.CreateStringPath(text, x, y)
|
||||||
|
rgc.Stroke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rgc *RasterGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
|
||||||
|
if err := rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), glyph, font.HintingNone); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range rgc.glyphBuf.Ends {
|
||||||
|
DrawContour(rgc, rgc.glyphBuf.Points[e0:e1], dx, dy)
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
|
||||||
|
// The text is placed so that the left edge of the em square of the first character of s
|
||||||
|
// and the baseline intersect at x, y. The majority of the affected pixels will be
|
||||||
|
// above and to the right of the point, but some may be below or to the left.
|
||||||
|
// For example, drawing a string that starts with a 'J' in an italic font may
|
||||||
|
// affect pixels below and left of the point.
|
||||||
|
func (rgc *RasterGraphicContext) CreateStringPath(s string, x, y float64) (cursor float64, err error) {
|
||||||
|
f := rgc.GetFont()
|
||||||
|
if f == nil {
|
||||||
|
err = errors.New("No font loaded, cannot continue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rgc.recalc()
|
||||||
|
|
||||||
|
startx := x
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rc := range s {
|
||||||
|
index := f.Index(rc)
|
||||||
|
if hasPrev {
|
||||||
|
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
err = rgc.drawGlyph(index, x, y)
|
||||||
|
if err != nil {
|
||||||
|
cursor = x - startx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
cursor = x - startx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringBounds returns the approximate pixel bounds of a string.
|
||||||
|
func (rgc *RasterGraphicContext) GetStringBounds(s string) (left, top, right, bottom float64, err error) {
|
||||||
|
f := rgc.GetFont()
|
||||||
|
if f == nil {
|
||||||
|
err = errors.New("No font loaded, cannot continue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rgc.recalc()
|
||||||
|
|
||||||
|
left = math.MaxFloat64
|
||||||
|
top = math.MaxFloat64
|
||||||
|
|
||||||
|
cursor := 0.0
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rc := range s {
|
||||||
|
index := f.Index(rc)
|
||||||
|
if hasPrev {
|
||||||
|
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), index, font.HintingNone); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range rgc.glyphBuf.Ends {
|
||||||
|
ps := rgc.glyphBuf.Points[e0:e1]
|
||||||
|
for _, p := range ps {
|
||||||
|
x, y := pointToF64Point(p)
|
||||||
|
top = math.Min(top, y)
|
||||||
|
bottom = math.Max(bottom, y)
|
||||||
|
left = math.Min(left, x+cursor)
|
||||||
|
right = math.Max(right, x+cursor)
|
||||||
|
}
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// recalc recalculates scale and bounds values from the font size, screen
|
||||||
|
// resolution and font metrics, and invalidates the glyph cache.
|
||||||
|
func (rgc *RasterGraphicContext) recalc() {
|
||||||
|
rgc.current.Scale = rgc.current.FontSizePoints * float64(rgc.DPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont sets the font used to draw text.
|
||||||
|
func (rgc *RasterGraphicContext) SetFont(font *truetype.Font) {
|
||||||
|
rgc.current.Font = font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the font used to draw text.
|
||||||
|
func (rgc *RasterGraphicContext) GetFont() *truetype.Font {
|
||||||
|
return rgc.current.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size in points (as in ``a 12 point font'').
|
||||||
|
func (rgc *RasterGraphicContext) SetFontSize(fontSizePoints float64) {
|
||||||
|
rgc.current.FontSizePoints = fontSizePoints
|
||||||
|
rgc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rgc *RasterGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
|
||||||
|
rgc.painter.SetColor(color)
|
||||||
|
rasterizer.Rasterize(rgc.painter)
|
||||||
|
rasterizer.Clear()
|
||||||
|
rgc.current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
|
func (rgc *RasterGraphicContext) Stroke(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.strokeRasterizer.UseNonZeroWinding = true
|
||||||
|
|
||||||
|
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
|
||||||
|
stroker.HalfLineWidth = rgc.current.LineWidth / 2
|
||||||
|
|
||||||
|
var liner Flattener
|
||||||
|
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
|
||||||
|
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
|
||||||
|
} else {
|
||||||
|
liner = stroker
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, liner, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
|
func (rgc *RasterGraphicContext) Fill(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
|
||||||
|
|
||||||
|
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, flattener, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
|
func (rgc *RasterGraphicContext) FillStroke(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
|
||||||
|
rgc.strokeRasterizer.UseNonZeroWinding = true
|
||||||
|
|
||||||
|
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
|
||||||
|
|
||||||
|
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
|
||||||
|
stroker.HalfLineWidth = rgc.current.LineWidth / 2
|
||||||
|
|
||||||
|
var liner Flattener
|
||||||
|
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
|
||||||
|
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
|
||||||
|
} else {
|
||||||
|
liner = stroker
|
||||||
|
}
|
||||||
|
|
||||||
|
demux := DemuxFlattener{Flatteners: []Flattener{flattener, liner}}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, demux, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
|
||||||
|
// Stroke
|
||||||
|
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
|
||||||
|
}
|
211
drawing/stack_graphic_context.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackGraphicContext is a context that does thngs.
|
||||||
|
type StackGraphicContext struct {
|
||||||
|
current *ContextStack
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextStack is a graphic context implementation.
|
||||||
|
type ContextStack struct {
|
||||||
|
Tr Matrix
|
||||||
|
Path *Path
|
||||||
|
LineWidth float64
|
||||||
|
Dash []float64
|
||||||
|
DashOffset float64
|
||||||
|
StrokeColor color.Color
|
||||||
|
FillColor color.Color
|
||||||
|
FillRule FillRule
|
||||||
|
Cap LineCap
|
||||||
|
Join LineJoin
|
||||||
|
|
||||||
|
FontSizePoints float64
|
||||||
|
Font *truetype.Font
|
||||||
|
|
||||||
|
Scale float64
|
||||||
|
|
||||||
|
Previous *ContextStack
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStackGraphicContext Create a new Graphic context from an image
|
||||||
|
func NewStackGraphicContext() *StackGraphicContext {
|
||||||
|
gc := &StackGraphicContext{}
|
||||||
|
gc.current = new(ContextStack)
|
||||||
|
gc.current.Tr = NewIdentityMatrix()
|
||||||
|
gc.current.Path = new(Path)
|
||||||
|
gc.current.LineWidth = 1.0
|
||||||
|
gc.current.StrokeColor = image.Black
|
||||||
|
gc.current.FillColor = image.White
|
||||||
|
gc.current.Cap = RoundCap
|
||||||
|
gc.current.FillRule = FillRuleEvenOdd
|
||||||
|
gc.current.Join = RoundJoin
|
||||||
|
gc.current.FontSizePoints = 10
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMatrixTransform returns the matrix transform.
|
||||||
|
func (gc *StackGraphicContext) GetMatrixTransform() Matrix {
|
||||||
|
return gc.current.Tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMatrixTransform sets the matrix transform.
|
||||||
|
func (gc *StackGraphicContext) SetMatrixTransform(tr Matrix) {
|
||||||
|
gc.current.Tr = tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeMatrixTransform composes a transform into the current transform.
|
||||||
|
func (gc *StackGraphicContext) ComposeMatrixTransform(tr Matrix) {
|
||||||
|
gc.current.Tr.Compose(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate rotates the matrix transform by an angle in degrees.
|
||||||
|
func (gc *StackGraphicContext) Rotate(angle float64) {
|
||||||
|
gc.current.Tr.Rotate(angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate translates a transform.
|
||||||
|
func (gc *StackGraphicContext) Translate(tx, ty float64) {
|
||||||
|
gc.current.Tr.Translate(tx, ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale scales a transform.
|
||||||
|
func (gc *StackGraphicContext) Scale(sx, sy float64) {
|
||||||
|
gc.current.Tr.Scale(sx, sy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStrokeColor sets the stroke color.
|
||||||
|
func (gc *StackGraphicContext) SetStrokeColor(c color.Color) {
|
||||||
|
gc.current.StrokeColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFillColor sets the fill color.
|
||||||
|
func (gc *StackGraphicContext) SetFillColor(c color.Color) {
|
||||||
|
gc.current.FillColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFillRule sets the fill rule.
|
||||||
|
func (gc *StackGraphicContext) SetFillRule(f FillRule) {
|
||||||
|
gc.current.FillRule = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineWidth sets the line width.
|
||||||
|
func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) {
|
||||||
|
gc.current.LineWidth = lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineCap sets the line cap.
|
||||||
|
func (gc *StackGraphicContext) SetLineCap(cap LineCap) {
|
||||||
|
gc.current.Cap = cap
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineJoin sets the line join.
|
||||||
|
func (gc *StackGraphicContext) SetLineJoin(join LineJoin) {
|
||||||
|
gc.current.Join = join
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineDash sets the line dash.
|
||||||
|
func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) {
|
||||||
|
gc.current.Dash = dash
|
||||||
|
gc.current.DashOffset = dashOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size.
|
||||||
|
func (gc *StackGraphicContext) SetFontSize(fontSizePoints float64) {
|
||||||
|
gc.current.FontSizePoints = fontSizePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFontSize gets the font size.
|
||||||
|
func (gc *StackGraphicContext) GetFontSize() float64 {
|
||||||
|
return gc.current.FontSizePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont sets the current font.
|
||||||
|
func (gc *StackGraphicContext) SetFont(f *truetype.Font) {
|
||||||
|
gc.current.Font = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the font.
|
||||||
|
func (gc *StackGraphicContext) GetFont() *truetype.Font {
|
||||||
|
return gc.current.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginPath starts a new path.
|
||||||
|
func (gc *StackGraphicContext) BeginPath() {
|
||||||
|
gc.current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns if the path is empty.
|
||||||
|
func (gc *StackGraphicContext) IsEmpty() bool {
|
||||||
|
return gc.current.Path.IsEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastPoint returns the last point on the path.
|
||||||
|
func (gc *StackGraphicContext) LastPoint() (x float64, y float64) {
|
||||||
|
return gc.current.Path.LastPoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo moves the cursor for a path.
|
||||||
|
func (gc *StackGraphicContext) MoveTo(x, y float64) {
|
||||||
|
gc.current.Path.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo draws a line.
|
||||||
|
func (gc *StackGraphicContext) LineTo(x, y float64) {
|
||||||
|
gc.current.Path.LineTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo draws a quad curve.
|
||||||
|
func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) {
|
||||||
|
gc.current.Path.QuadCurveTo(cx, cy, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubicCurveTo draws a cubic curve.
|
||||||
|
func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
|
gc.current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo draws an arc.
|
||||||
|
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
|
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes a path.
|
||||||
|
func (gc *StackGraphicContext) Close() {
|
||||||
|
gc.current.Path.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save pushes a context onto the stack.
|
||||||
|
func (gc *StackGraphicContext) Save() {
|
||||||
|
context := new(ContextStack)
|
||||||
|
context.FontSizePoints = gc.current.FontSizePoints
|
||||||
|
context.Font = gc.current.Font
|
||||||
|
context.LineWidth = gc.current.LineWidth
|
||||||
|
context.StrokeColor = gc.current.StrokeColor
|
||||||
|
context.FillColor = gc.current.FillColor
|
||||||
|
context.FillRule = gc.current.FillRule
|
||||||
|
context.Dash = gc.current.Dash
|
||||||
|
context.DashOffset = gc.current.DashOffset
|
||||||
|
context.Cap = gc.current.Cap
|
||||||
|
context.Join = gc.current.Join
|
||||||
|
context.Path = gc.current.Path.Copy()
|
||||||
|
context.Font = gc.current.Font
|
||||||
|
context.Scale = gc.current.Scale
|
||||||
|
copy(context.Tr[:], gc.current.Tr[:])
|
||||||
|
context.Previous = gc.current
|
||||||
|
gc.current = context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the previous context.
|
||||||
|
func (gc *StackGraphicContext) Restore() {
|
||||||
|
if gc.current.Previous != nil {
|
||||||
|
oldContext := gc.current
|
||||||
|
gc.current = gc.current.Previous
|
||||||
|
oldContext.Previous = nil
|
||||||
|
}
|
||||||
|
}
|
85
drawing/stroker.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright 2010 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 13/12/2010 by Laurent Le Goff
|
||||||
|
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// NewLineStroker creates a new line stroker.
|
||||||
|
func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker {
|
||||||
|
l := new(LineStroker)
|
||||||
|
l.Flattener = flattener
|
||||||
|
l.HalfLineWidth = 0.5
|
||||||
|
l.Cap = c
|
||||||
|
l.Join = j
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineStroker draws the stroke portion of a line.
|
||||||
|
type LineStroker struct {
|
||||||
|
Flattener Flattener
|
||||||
|
HalfLineWidth float64
|
||||||
|
Cap LineCap
|
||||||
|
Join LineJoin
|
||||||
|
vertices []float64
|
||||||
|
rewind []float64
|
||||||
|
x, y, nx, ny float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (l *LineStroker) MoveTo(x, y float64) {
|
||||||
|
l.x, l.y = x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (l *LineStroker) LineTo(x, y float64) {
|
||||||
|
l.line(l.x, l.y, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (l *LineStroker) LineJoin() {}
|
||||||
|
|
||||||
|
func (l *LineStroker) line(x1, y1, x2, y2 float64) {
|
||||||
|
dx := (x2 - x1)
|
||||||
|
dy := (y2 - y1)
|
||||||
|
d := vectorDistance(dx, dy)
|
||||||
|
if d != 0 {
|
||||||
|
nx := dy * l.HalfLineWidth / d
|
||||||
|
ny := -(dx * l.HalfLineWidth / d)
|
||||||
|
l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny)
|
||||||
|
l.x, l.y, l.nx, l.ny = x2, y2, nx, ny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (l *LineStroker) Close() {
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (l *LineStroker) End() {
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.Flattener.MoveTo(l.vertices[0], l.vertices[1])
|
||||||
|
for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 {
|
||||||
|
l.Flattener.LineTo(l.vertices[i], l.vertices[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 {
|
||||||
|
l.Flattener.LineTo(l.rewind[i], l.rewind[j])
|
||||||
|
}
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.Flattener.LineTo(l.vertices[0], l.vertices[1])
|
||||||
|
}
|
||||||
|
l.Flattener.End()
|
||||||
|
// reinit vertices
|
||||||
|
l.vertices = l.vertices[0:0]
|
||||||
|
l.rewind = l.rewind[0:0]
|
||||||
|
l.x, l.y, l.nx, l.ny = 0, 0, 0, 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LineStroker) appendVertex(vertices ...float64) {
|
||||||
|
s := len(vertices) / 2
|
||||||
|
l.vertices = append(l.vertices, vertices[:s]...)
|
||||||
|
l.rewind = append(l.rewind, vertices[s:]...)
|
||||||
|
}
|
67
drawing/text.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DrawContour draws the given closed contour at the given sub-pixel offset.
|
||||||
|
func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startX, startY := pointToF64Point(ps[0])
|
||||||
|
path.MoveTo(startX+dx, startY+dy)
|
||||||
|
q0X, q0Y, on0 := startX, startY, true
|
||||||
|
for _, p := range ps[1:] {
|
||||||
|
qX, qY := pointToF64Point(p)
|
||||||
|
on := p.Flags&0x01 != 0
|
||||||
|
if on {
|
||||||
|
if on0 {
|
||||||
|
path.LineTo(qX+dx, qY+dy)
|
||||||
|
} else {
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
|
||||||
|
}
|
||||||
|
} else if !on0 {
|
||||||
|
midX := (q0X + qX) / 2
|
||||||
|
midY := (q0Y + qY) / 2
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
|
||||||
|
}
|
||||||
|
q0X, q0Y, on0 = qX, qY, on
|
||||||
|
}
|
||||||
|
// Close the curve.
|
||||||
|
if on0 {
|
||||||
|
path.LineTo(startX+dx, startY+dy)
|
||||||
|
} else {
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontExtents contains font metric information.
|
||||||
|
type FontExtents struct {
|
||||||
|
// Ascent is the distance that the text
|
||||||
|
// extends above the baseline.
|
||||||
|
Ascent float64
|
||||||
|
|
||||||
|
// Descent is the distance that the text
|
||||||
|
// extends below the baseline. The descent
|
||||||
|
// is given as a negative value.
|
||||||
|
Descent float64
|
||||||
|
|
||||||
|
// Height is the distance from the lowest
|
||||||
|
// descending point to the highest ascending
|
||||||
|
// point.
|
||||||
|
Height float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extents returns the FontExtents for a font.
|
||||||
|
// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
|
||||||
|
func Extents(font *truetype.Font, size float64) FontExtents {
|
||||||
|
bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
|
||||||
|
scale := size / float64(font.FUnitsPerEm())
|
||||||
|
return FontExtents{
|
||||||
|
Ascent: float64(bounds.Max.Y) * scale,
|
||||||
|
Descent: float64(bounds.Min.Y) * scale,
|
||||||
|
Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
|
||||||
|
}
|
||||||
|
}
|
39
drawing/transformer.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2010 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 13/12/2010 by Laurent Le Goff
|
||||||
|
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// Transformer apply the Matrix transformation tr
|
||||||
|
type Transformer struct {
|
||||||
|
Tr Matrix
|
||||||
|
Flattener Flattener
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (t Transformer) MoveTo(x, y float64) {
|
||||||
|
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
|
||||||
|
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
|
||||||
|
t.Flattener.MoveTo(u, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (t Transformer) LineTo(x, y float64) {
|
||||||
|
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
|
||||||
|
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
|
||||||
|
t.Flattener.LineTo(u, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (t Transformer) LineJoin() {
|
||||||
|
t.Flattener.LineJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (t Transformer) Close() {
|
||||||
|
t.Flattener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (t Transformer) End() {
|
||||||
|
t.Flattener.End()
|
||||||
|
}
|
68
drawing/util.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PixelsToPoints returns the points for a given number of pixels at a DPI.
|
||||||
|
func PixelsToPoints(dpi, pixels float64) (points float64) {
|
||||||
|
points = (pixels * 72.0) / dpi
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointsToPixels returns the pixels for a given number of points at a DPI.
|
||||||
|
func PointsToPixels(dpi, points float64) (pixels float64) {
|
||||||
|
pixels = (points * dpi) / 72.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(i int) int {
|
||||||
|
if i < 0 {
|
||||||
|
return -i
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(x1, y1, x2, y2 float64) float64 {
|
||||||
|
return vectorDistance(x2-x1, y2-y1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorDistance(dx, dy float64) float64 {
|
||||||
|
return float64(math.Sqrt(dx*dx + dy*dy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFtCap(c LineCap) raster.Capper {
|
||||||
|
switch c {
|
||||||
|
case RoundCap:
|
||||||
|
return raster.RoundCapper
|
||||||
|
case ButtCap:
|
||||||
|
return raster.ButtCapper
|
||||||
|
case SquareCap:
|
||||||
|
return raster.SquareCapper
|
||||||
|
}
|
||||||
|
return raster.RoundCapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFtJoin(j LineJoin) raster.Joiner {
|
||||||
|
switch j {
|
||||||
|
case RoundJoin:
|
||||||
|
return raster.RoundJoiner
|
||||||
|
case BevelJoin:
|
||||||
|
return raster.BevelJoiner
|
||||||
|
}
|
||||||
|
return raster.RoundJoiner
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointToF64Point(p truetype.Point) (x, y float64) {
|
||||||
|
return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fUnitsToFloat64(x fixed.Int26_6) float64 {
|
||||||
|
scaled := x << 2
|
||||||
|
return float64(scaled/256) + float64(scaled%256)/256.0
|
||||||
|
}
|
131
ema_series.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultEMAPeriod is the default EMA period used in the sigma calculation.
|
||||||
|
DefaultEMAPeriod = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*EMASeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*EMASeries)(nil)
|
||||||
|
_ LastValuesProvider = (*EMASeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EMASeries is a computed series.
|
||||||
|
type EMASeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Period int
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
cache []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (ema EMASeries) GetName() string {
|
||||||
|
return ema.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (ema EMASeries) GetStyle() Style {
|
||||||
|
return ema.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (ema EMASeries) GetYAxis() YAxisType {
|
||||||
|
return ema.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (ema EMASeries) GetPeriod() int {
|
||||||
|
if ema.Period == 0 {
|
||||||
|
return DefaultEMAPeriod
|
||||||
|
}
|
||||||
|
return ema.Period
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (ema EMASeries) Len() int {
|
||||||
|
return ema.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigma returns the smoothing factor for the serise.
|
||||||
|
func (ema EMASeries) GetSigma() float64 {
|
||||||
|
return 2.0 / (float64(ema.GetPeriod()) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (ema *EMASeries) GetValues(index int) (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
vx, _ := ema.InnerSeries.GetValues(index)
|
||||||
|
x = vx
|
||||||
|
y = ema.cache[index]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first moving average value.
|
||||||
|
func (ema *EMASeries) GetFirstValues() (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
x, _ = ema.InnerSeries.GetValues(0)
|
||||||
|
y = ema.cache[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last moving average value but walking back window size samples,
|
||||||
|
// and recomputing the last moving average chunk.
|
||||||
|
func (ema *EMASeries) GetLastValues() (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
lastIndex := ema.InnerSeries.Len() - 1
|
||||||
|
x, _ = ema.InnerSeries.GetValues(lastIndex)
|
||||||
|
y = ema.cache[lastIndex]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ema *EMASeries) ensureCachedValues() {
|
||||||
|
seriesLength := ema.InnerSeries.Len()
|
||||||
|
ema.cache = make([]float64, seriesLength)
|
||||||
|
sigma := ema.GetSigma()
|
||||||
|
for x := 0; x < seriesLength; x++ {
|
||||||
|
_, y := ema.InnerSeries.GetValues(x)
|
||||||
|
if x == 0 {
|
||||||
|
ema.cache[x] = y
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
previousEMA := ema.cache[x-1]
|
||||||
|
ema.cache[x] = ((y - previousEMA) * sigma) + previousEMA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := ema.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (ema *EMASeries) Validate() error {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("ema series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
105
ema_series_test.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
emaXValues = LinearRange(1.0, 50.0)
|
||||||
|
emaYValues = []float64{
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
1, 2,
|
||||||
|
}
|
||||||
|
emaExpected = []float64{
|
||||||
|
1,
|
||||||
|
1.074074074,
|
||||||
|
1.216735254,
|
||||||
|
1.422903013,
|
||||||
|
1.68787316,
|
||||||
|
1.859141815,
|
||||||
|
1.943649828,
|
||||||
|
1.947823915,
|
||||||
|
1.877614736,
|
||||||
|
1.886680311,
|
||||||
|
1.969148437,
|
||||||
|
2.119581886,
|
||||||
|
2.33294619,
|
||||||
|
2.456431658,
|
||||||
|
2.496695979,
|
||||||
|
2.459903685,
|
||||||
|
2.351762671,
|
||||||
|
2.325706177,
|
||||||
|
2.375653867,
|
||||||
|
2.495975803,
|
||||||
|
2.681459077,
|
||||||
|
2.779128775,
|
||||||
|
2.795489607,
|
||||||
|
2.73656445,
|
||||||
|
2.607930047,
|
||||||
|
2.562898191,
|
||||||
|
2.595276103,
|
||||||
|
2.699329725,
|
||||||
|
2.869749746,
|
||||||
|
2.953471987,
|
||||||
|
2.956918506,
|
||||||
|
2.886035654,
|
||||||
|
2.746329309,
|
||||||
|
2.691045657,
|
||||||
|
2.713931163,
|
||||||
|
2.809195522,
|
||||||
|
2.971477335,
|
||||||
|
3.047664199,
|
||||||
|
3.044133518,
|
||||||
|
2.966790294,
|
||||||
|
2.821102124,
|
||||||
|
2.760279745,
|
||||||
|
2.778036801,
|
||||||
|
2.868552593,
|
||||||
|
3.026437586,
|
||||||
|
3.098553321,
|
||||||
|
3.091253075,
|
||||||
|
3.010419514,
|
||||||
|
2.86149955,
|
||||||
|
2.797684768,
|
||||||
|
}
|
||||||
|
emaDelta = 0.0001
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEMASeries(t *testing.T) {
|
||||||
|
// replaced new assertions helper
|
||||||
|
|
||||||
|
mockSeries := mockValuesProvider{
|
||||||
|
emaXValues,
|
||||||
|
emaYValues,
|
||||||
|
}
|
||||||
|
testutil.AssertEqual(t, 50, mockSeries.Len())
|
||||||
|
|
||||||
|
ema := &EMASeries{
|
||||||
|
InnerSeries: mockSeries,
|
||||||
|
Period: 26,
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := ema.GetSigma()
|
||||||
|
testutil.AssertEqual(t, 2.0/(26.0+1), sig)
|
||||||
|
|
||||||
|
var yvalues []float64
|
||||||
|
for x := 0; x < ema.Len(); x++ {
|
||||||
|
_, y := ema.GetValues(x)
|
||||||
|
yvalues = append(yvalues, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, yv := range yvalues {
|
||||||
|
testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
lvx, lvy := ema.GetLastValues()
|
||||||
|
testutil.AssertEqual(t, 50.0, lvx)
|
||||||
|
testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta)
|
||||||
|
}
|
44
examples/annotations/main.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
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).
|
||||||
|
|
||||||
|
It is important to not that the chart automatically sizes the canvas box to fit the annotations,
|
||||||
|
As well as automatically assign a series color for the `Stroke` or border component of the series.
|
||||||
|
|
||||||
|
The annotation series is most often used by the original author to show the last value of another series, but
|
||||||
|
they can be used in other capacities as well.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
chart.AnnotationSeries{
|
||||||
|
Annotations: []chart.Value2{
|
||||||
|
{XValue: 1.0, YValue: 1.0, Label: "One"},
|
||||||
|
{XValue: 2.0, YValue: 2.0, Label: "Two"},
|
||||||
|
{XValue: 3.0, YValue: 3.0, Label: "Three"},
|
||||||
|
{XValue: 4.0, YValue: 4.0, Label: "Four"},
|
||||||
|
{XValue: 5.0, YValue: 5.0, Label: "Five"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
f, _ := os.Create("output.png")
|
||||||
|
defer f.Close()
|
||||||
|
graph.Render(chart.PNG, f)
|
||||||
|
}
|
BIN
examples/annotations/output.png
Normal file
After Width: | Height: | Size: 26 KiB |
34
examples/axes/main.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
chart "github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||||
|
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/axes/output.png
Normal file
After Width: | Height: | Size: 22 KiB |
40
examples/axes_labels/main.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
chart "github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||||
|
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
XAxis: chart.XAxis{
|
||||||
|
Name: "The XAxis",
|
||||||
|
},
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
Name: "The YAxis",
|
||||||
|
},
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/axes_labels/output.png
Normal file
After Width: | Height: | Size: 24 KiB |
35
examples/bar_chart/main.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/bar_chart/output.png
Normal file
After Width: | Height: | Size: 19 KiB |
62
examples/bar_chart_base_value/main.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/bar_chart_base_value/output.png
Normal file
After Width: | Height: | Size: 18 KiB |
23
examples/basic/main.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/basic/output.png
Normal file
After Width: | Height: | Size: 24 KiB |
52
examples/benchmark_line_charts/main.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/benchmark_line_charts/output.png
Normal file
After Width: | Height: | Size: 314 KiB |
59
examples/css_classes/main.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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(
|
||||||
|
"<!DOCTYPE html><html><head>" +
|
||||||
|
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/main.css\">" +
|
||||||
|
"</head>" +
|
||||||
|
"<body>"))
|
||||||
|
|
||||||
|
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("</body>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
38
examples/custom_formatters/main.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
This example also shows what the chart looks like with the x-axis left off or not shown.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
ValueFormatter: func(v interface{}) string {
|
||||||
|
if vf, isFloat := v.(float64); isFloat {
|
||||||
|
return fmt.Sprintf("%0.6f", vf)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/custom_formatters/output.png
Normal file
After Width: | Height: | Size: 29 KiB |
34
examples/custom_padding/main.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/custom_padding/output.png
Normal file
After Width: | Height: | Size: 65 KiB |
34
examples/custom_ranges/main.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
Range: &chart.ContinuousRange{
|
||||||
|
Min: 0.0,
|
||||||
|
Max: 10.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/custom_ranges/output.png
Normal file
After Width: | Height: | Size: 20 KiB |
38
examples/custom_styles/main.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
In this example we set some custom colors for the series and the chart background and canvas.
|
||||||
|
*/
|
||||||
|
graph := chart.Chart{
|
||||||
|
Background: chart.Style{
|
||||||
|
FillColor: drawing.ColorBlue,
|
||||||
|
},
|
||||||
|
Canvas: chart.Style{
|
||||||
|
FillColor: drawing.ColorFromHex("efefef"),
|
||||||
|
},
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
StrokeColor: drawing.ColorRed, // will supercede defaults
|
||||||
|
FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/custom_styles/output.png
Normal file
After Width: | Height: | Size: 23 KiB |
21
examples/custom_stylesheets/inlineOutput.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">\n<style type="text/css"><![CDATA[svg .background { fill: white; }svg .canvas { fill: white; }svg path.blue { fill: blue; stroke: lightblue; }svg path.green { fill: green; stroke: lightgreen; }svg path.gray { fill: gray; stroke: lightgray; }svg text.blue { fill: white; }svg text.green { fill: white; }svg text.gray { fill: white; }]]></style><path d="M 0 0
|
||||||
|
L 512 0
|
||||||
|
L 512 512
|
||||||
|
L 0 512
|
||||||
|
L 0 0" class="background"/><path d="M 5 5
|
||||||
|
L 507 5
|
||||||
|
L 507 507
|
||||||
|
L 5 507
|
||||||
|
L 5 5" class="canvas"/><path d="M 256 256
|
||||||
|
L 507 256
|
||||||
|
A 251 251 128.56 0 1 100 452
|
||||||
|
L 256 256
|
||||||
|
Z" class="blue"/><path d="M 256 256
|
||||||
|
L 100 452
|
||||||
|
A 251 251 128.56 0 1 201 12
|
||||||
|
L 256 256
|
||||||
|
Z" class="green"/><path d="M 256 256
|
||||||
|
L 201 12
|
||||||
|
A 251 251 102.85 0 1 506 256
|
||||||
|
L 256 256
|
||||||
|
Z" class="gray"/><text x="313" y="413" class="blue">Blue</text><text x="73" y="226" class="green">Green</text><text x="344" y="133" class="gray">Gray</text></svg>
|
After Width: | Height: | Size: 987 B |
88
examples/custom_stylesheets/main.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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(
|
||||||
|
`<?xml version="1.0" standalone="no"?>` +
|
||||||
|
`<?xml-stylesheet href="/main.css" type="text/css"?>` +
|
||||||
|
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`))
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
42
examples/custom_ticks/main.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/custom_ticks/output.png
Normal file
After Width: | Height: | Size: 17 KiB |
49
examples/descending/main.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||||
|
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically,
|
||||||
|
the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
|
||||||
|
Additionally, it shows how you can use the "Descending" property of continuous ranges to change the ordering of
|
||||||
|
how values (including ticks) are drawn.
|
||||||
|
*/
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
Height: 500,
|
||||||
|
Width: 500,
|
||||||
|
XAxis: chart.XAxis{
|
||||||
|
/*Range: &chart.ContinuousRange{
|
||||||
|
Descending: true,
|
||||||
|
},*/
|
||||||
|
},
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
Range: &chart.ContinuousRange{
|
||||||
|
Descending: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
BIN
examples/descending/output.png
Normal file
After Width: | Height: | Size: 19 KiB |
28
examples/donut_chart/main.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run main.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
BIN
examples/donut_chart/output.png
Normal file
After Width: | Height: | Size: 24 KiB |
25
examples/donut_chart/reg.svg
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">\n<path d="M 0 0
|
||||||
|
L 512 0
|
||||||
|
L 512 512
|
||||||
|
L 0 512
|
||||||
|
L 0 0" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 5 5
|
||||||
|
L 507 5
|
||||||
|
L 507 507
|
||||||
|
L 5 507
|
||||||
|
L 5 5" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 256 256
|
||||||
|
L 438 256
|
||||||
|
A 182 182 225.00 1 1 127 127
|
||||||
|
L 256 256
|
||||||
|
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(106,195,203,1.0)"/><path d="M 256 256
|
||||||
|
L 127 127
|
||||||
|
A 182 182 90.00 0 1 385 127
|
||||||
|
L 256 256
|
||||||
|
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(42,190,137,1.0)"/><path d="M 256 256
|
||||||
|
L 385 127
|
||||||
|
A 182 182 45.00 0 1 438 256
|
||||||
|
L 256 256
|
||||||
|
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(110,128,139,1.0)"/><path d="M 256 256
|
||||||
|
L 321 256
|
||||||
|
A 65 65 359.00 1 1 321 255
|
||||||
|
L 256 256
|
||||||
|
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><text x="159" y="461" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Blue</text><text x="241" y="48" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Two</text><text x="440" y="181" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">One</text></svg>
|
After Width: | Height: | Size: 1.4 KiB |
222
examples/horizontal_stacked_bar/main.go
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/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)
|
||||||
|
}
|
||||||
|
}
|
BIN
examples/horizontal_stacked_bar/output.png
Normal file
After Width: | Height: | Size: 34 KiB |