Compare commits
77 commits
pointers-a
...
main
Author | SHA1 | Date | |
---|---|---|---|
63f0be79dd | |||
1f6d76e8b5 | |||
cc8d36edd9 | |||
bd2b44aa38 | |||
|
b46667ea80 | ||
|
218e744a87 | ||
|
a334e8e43a | ||
|
c9c9042154 | ||
|
2f3402adfb | ||
|
dd823617d9 | ||
|
7281bbdfa8 | ||
|
b3fc6cba9f | ||
|
0d3588f719 | ||
|
d9bba03c8f | ||
|
c6416e0757 | ||
|
f6d9d1edca | ||
|
2c2fb0651b | ||
|
2ff54048b8 | ||
|
e781e0cd22 | ||
|
54fc699377 | ||
|
1ccfbb0172 | ||
|
c1468e8ae4 | ||
|
962b9abdec | ||
|
3a7bc55431 | ||
|
60baf17927 | ||
|
45fad0cfb8 | ||
|
2d5aeaf824 | ||
|
6d57cf4533 | ||
|
602ff901f7 | ||
|
fed210cc81 | ||
|
762b314e86 | ||
|
07a9cdf513 | ||
|
9852fce5a1 | ||
|
59451fbeb4 | ||
|
0576aba75e | ||
|
781a45d770 | ||
|
fa93bd8abb | ||
|
5f42a580a9 | ||
|
26eaa1d898 | ||
|
3cb33d48d3 | ||
|
96acfc6a9f | ||
|
31d235310c | ||
|
70d5b73afd | ||
|
f5889c93ae | ||
|
4e6c06ca87 | ||
|
3e352f140b | ||
|
f97f94425f | ||
|
6735e8990a | ||
|
865ff54ab9 | ||
|
0fb4aa53e9 | ||
|
828d1952d8 | ||
|
872b97b99f | ||
|
a7ff82d63f | ||
|
0e849b11bb | ||
|
1a09989055 | ||
|
1555902fc4 | ||
|
1144b80a46 | ||
|
1f159d195f | ||
|
4ed65028e4 | ||
|
ac680bd82d | ||
|
3edccc4758 | ||
|
d667b8c983 | ||
|
0506f74600 | ||
|
9e3a080aa3 | ||
|
44990c63ed | ||
|
1ebbcf493d | ||
|
62338336c3 | ||
|
2dc8482db3 | ||
|
7c3982fe3d | ||
|
70e6cfddc5 | ||
|
df14434b6e | ||
|
7d28470055 | ||
|
11e380634b | ||
|
f72f7fd57b | ||
|
a0ea012903 | ||
|
34de44488d | ||
|
03708a90ef |
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 ./...
|
19
.gitignore
vendored
|
@ -1 +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
|
||||
|
|
13
.travis.yml
|
@ -1,13 +0,0 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.6.2
|
||||
|
||||
sudo: false
|
||||
|
||||
before_script:
|
||||
- go get -u github.com/blendlabs/go-assert
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
- go test ./...
|
1
COVERAGE
Normal file
|
@ -0,0 +1 @@
|
|||
29.02
|
1
LICENSE
|
@ -1,6 +1,7 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 William Charczuk.
|
||||
Copyright (c) 2024 Zeni Kim.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
15
Makefile
|
@ -1,9 +1,10 @@
|
|||
all: test
|
||||
all: new-install test
|
||||
|
||||
new-install:
|
||||
@go get -v -u ./...
|
||||
|
||||
generate:
|
||||
@go generate ./...
|
||||
|
||||
test:
|
||||
@go test ./...
|
||||
|
||||
cover:
|
||||
@go test -short -covermode=set -coverprofile=profile.cov
|
||||
@go tool cover -html=profile.cov
|
||||
@rm profile.cov
|
||||
@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"
|
39
README.md
|
@ -1,55 +1,55 @@
|
|||
go-chart
|
||||
========
|
||||
[![Build Status](https://travis-ci.org/wcharczuk/go-chart.svg?branch=master)](https://travis-ci.org/wcharczuk/go-chart)[![Go Report Card](https://goreportcard.com/badge/github.com/wcharczuk/go-chart)](https://goreportcard.com/report/github.com/wcharczuk/go-chart)
|
||||
|
||||
Package `chart` is a very simple golang native charting library that supports timeseries and continuous
|
||||
line charts.
|
||||
This project starts from a full copy from [https://git.smarteching.com/zeni/go-chart](https://git.smarteching.com/zeni/go-chart). 28 Oct 2024.
|
||||
|
||||
The v1.0 release has been tagged so things should be more or less stable, if something changes please log an issue.
|
||||
-
|
||||
|
||||
Master should now be on the v2.x codebase, which brings a couple new features and better handling of basics like axes labeling etc. Per usual, see `_examples` for more information.
|
||||
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 -u github.com/wcharczuk/go-chart
|
||||
> go get git.smarteching.com/zeni/go-chart/v2@latest
|
||||
```
|
||||
|
||||
Most of the components are interchangeable so feel free to crib whatever you want.
|
||||
Most of the components are interchangeable so feel free to crib whatever you want.
|
||||
|
||||
# Output Examples
|
||||
# Output Examples
|
||||
|
||||
Spark Lines:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/tvix_ltm.png)
|
||||
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/tvix_ltm.png)
|
||||
|
||||
Single axis:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/goog_ltm.png)
|
||||
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/goog_ltm.png)
|
||||
|
||||
Two axis:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/two_axis.png)
|
||||
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/two_axis.png)
|
||||
|
||||
# Other Chart Types
|
||||
|
||||
Pie Chart:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/pie_chart.png)
|
||||
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/pie_chart.png)
|
||||
|
||||
The code for this chart can be found in `_examples/pie_chart/main.go`.
|
||||
The code for this chart can be found in `examples/pie_chart/main.go`.
|
||||
|
||||
Stacked Bar:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/stacked_bar.png)
|
||||
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/stacked_bar.png)
|
||||
|
||||
The code for this chart can be found in `_examples/stacked_bar/main.go`.
|
||||
The code for this chart can be found in `examples/stacked_bar/main.go`.
|
||||
|
||||
# Code Examples
|
||||
|
||||
Actual chart configurations and examples can be found in the `./_examples/` directory. They are web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output.
|
||||
Actual chart configurations and examples can be found in the `./examples/` directory. They are simple CLI programs that write to `output.png` (they are also updated with `go generate`.
|
||||
|
||||
If folder ends in "web", has web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output.
|
||||
|
||||
# Usage
|
||||
|
||||
|
@ -61,7 +61,7 @@ import (
|
|||
...
|
||||
"bytes"
|
||||
...
|
||||
"github.com/wcharczuk/go-chart" //exposes "chart"
|
||||
"git.smarteching.com/zeni/go-chart/v2" //exposes "chart"
|
||||
)
|
||||
|
||||
graph := chart.Chart{
|
||||
|
@ -83,8 +83,7 @@ Here, we have a single series with x range values as float64s, rendered to a PNG
|
|||
|
||||
# API Overview
|
||||
|
||||
Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default. One complication here
|
||||
is any object's root `chart.Style` object (i.e named `Style`) and the `Show` property specifically, if any other property is set and the `Show` property is unset, it is assumed to be it's default value of `False`.
|
||||
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.
|
||||
|
||||
|
@ -96,4 +95,4 @@ The goal with the API itself is to have the "zero value be useful", and to requi
|
|||
|
||||
# Contributions
|
||||
|
||||
This library is super early but contributions are welcome.
|
||||
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
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,43 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func drawChartWide(res http.ResponseWriter, req *http.Request) {
|
||||
graph := chart.Chart{
|
||||
Width: 1920, //this overrides the default.
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.HandleFunc("/wide", drawChartWide)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
Before Width: | Height: | Size: 12 KiB |
|
@ -1,73 +0,0 @@
|
|||
// Usage: http://localhost:8080?series=100&values=1000
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func random(min, max float64) float64 {
|
||||
return rand.Float64()*(max-min) + min
|
||||
}
|
||||
|
||||
func drawLargeChart(res http.ResponseWriter, r *http.Request) {
|
||||
numSeriesInt64, err := strconv.ParseInt(r.FormValue("series"), 10, 64)
|
||||
if err != nil {
|
||||
numSeriesInt64 = int64(1)
|
||||
}
|
||||
numSeries := int(numSeriesInt64)
|
||||
|
||||
numValuesInt64, err := strconv.ParseInt(r.FormValue("values"), 10, 64)
|
||||
if err != nil {
|
||||
numValuesInt64 = int64(100)
|
||||
}
|
||||
numValues := int(numValuesInt64)
|
||||
|
||||
series := make([]chart.Series, numSeries)
|
||||
|
||||
for i := 0; i < numSeries; i++ {
|
||||
xValues := make([]time.Time, numValues)
|
||||
yValues := make([]float64, numValues)
|
||||
|
||||
for j := 0; j < numValues; j++ {
|
||||
xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1)
|
||||
yValues[j] = random(float64(-500), float64(500))
|
||||
}
|
||||
|
||||
series[i] = chart.TimeSeries{
|
||||
Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i),
|
||||
XValues: xValues,
|
||||
YValues: yValues,
|
||||
}
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Name: "Time",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "Value",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: series,
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawLargeChart)
|
||||
http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Write([]byte{})
|
||||
})
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 27 KiB |
|
@ -1,74 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
graph := chart.Chart{
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 50,
|
||||
Left: 25,
|
||||
Right: 25,
|
||||
Bottom: 10,
|
||||
},
|
||||
FillColor: drawing.ColorFromHex("efefef"),
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func drawChartDefault(res http.ResponseWriter, req *http.Request) {
|
||||
graph := chart.Chart{
|
||||
Background: chart.Style{
|
||||
FillColor: drawing.ColorFromHex("efefef"),
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.HandleFunc("/default", drawChartDefault)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 60 KiB |
|
@ -1,44 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
start := chart.Date.Date(2016, 7, 01, chart.Date.Eastern())
|
||||
end := chart.Date.Date(2016, 07, 21, chart.Date.Eastern())
|
||||
xv := chart.Sequence.MarketHours(start, end, chart.NYSEOpen, chart.NYSEClose, chart.Date.IsNYSEHoliday)
|
||||
yv := chart.Sequence.RandomWithAverage(len(xv), 200, 10)
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
TickPosition: chart.TickPositionBetweenTicks,
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
Range: &chart.MarketHoursRange{
|
||||
MarketOpen: chart.NYSEOpen,
|
||||
MarketClose: chart.NYSEClose,
|
||||
HolidayProvider: chart.Date.IsNYSEHoliday,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.TimeSeries{
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 170 KiB |
Before Width: | Height: | Size: 34 KiB |
|
@ -1,41 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
||||
/*
|
||||
In this example we add a new type of series, a `PolynomialRegressionSeries` that takes another series as a required argument.
|
||||
InnerSeries only needs to implement `ValueProvider`, so really you could chain `PolynomialRegressionSeries` together if you wanted.
|
||||
*/
|
||||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
polyRegSeries := &chart.PolynomialRegressionSeries{
|
||||
Degree: 3,
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
mainSeries,
|
||||
polyRegSeries,
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func parseInt(str string) int {
|
||||
v, _ := strconv.Atoi(str)
|
||||
return v
|
||||
}
|
||||
|
||||
func parseFloat64(str string) float64 {
|
||||
v, _ := strconv.ParseFloat(str, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func readData() ([]time.Time, []float64) {
|
||||
var xvalues []time.Time
|
||||
var yvalues []float64
|
||||
err := chart.File.ReadByLines("requests.csv", func(line string) {
|
||||
parts := strings.Split(line, ",")
|
||||
year := parseInt(parts[0])
|
||||
month := parseInt(parts[1])
|
||||
day := parseInt(parts[2])
|
||||
hour := parseInt(parts[3])
|
||||
elapsedMillis := parseFloat64(parts[4])
|
||||
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
||||
yvalues = append(yvalues, elapsedMillis)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
return xvalues, yvalues
|
||||
}
|
||||
|
||||
func releases() []chart.GridLine {
|
||||
return []chart.GridLine{
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))},
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
|
||||
}
|
||||
}
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
xvalues, yvalues := readData()
|
||||
mainSeries := chart.TimeSeries{
|
||||
Name: "Prod Request Timings",
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.ColorBlue,
|
||||
FillColor: chart.ColorBlue.WithAlpha(100),
|
||||
},
|
||||
XValues: xvalues,
|
||||
YValues: yvalues,
|
||||
}
|
||||
|
||||
linreg := &chart.LinearRegressionSeries{
|
||||
Name: "Linear Regression",
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.ColorAlternateBlue,
|
||||
StrokeDashArray: []float64{5.0, 5.0},
|
||||
},
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
|
||||
sma := &chart.SMASeries{
|
||||
Name: "SMA",
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.ColorRed,
|
||||
StrokeDashArray: []float64{5.0, 5.0},
|
||||
},
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 50,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "Elapsed Millis",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
TickStyle: chart.Style{
|
||||
TextRotationDegrees: 45.0,
|
||||
},
|
||||
ValueFormatter: func(v interface{}) string {
|
||||
return fmt.Sprintf("%d ms", int(v.(float64)))
|
||||
},
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
GridMajorStyle: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.ColorAlternateGray,
|
||||
StrokeWidth: 1.0,
|
||||
},
|
||||
GridLines: releases(),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
mainSeries,
|
||||
linreg,
|
||||
chart.LastValueAnnotation(linreg),
|
||||
sma,
|
||||
chart.LastValueAnnotation(sma),
|
||||
},
|
||||
}
|
||||
|
||||
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
|
||||
|
||||
res.Header().Set("Content-Type", chart.ContentTypeSVG)
|
||||
graph.Render(chart.SVG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 27 KiB |
|
@ -1,42 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
||||
/*
|
||||
In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument.
|
||||
InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
|
||||
*/
|
||||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
// note we create a SimpleMovingAverage series by assignin the inner series.
|
||||
// we need to use a reference because `.Render()` needs to modify state within the series.
|
||||
smaSeries := &chart.SMASeries{
|
||||
InnerSeries: mainSeries,
|
||||
} // we can optionally set the `WindowSize` property which alters how the moving average is calculated.
|
||||
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
mainSeries,
|
||||
smaSeries,
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB |
|
@ -5,6 +5,11 @@ import (
|
|||
"math"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*AnnotationSeries)(nil)
|
||||
)
|
||||
|
||||
// AnnotationSeries is a series of labels on the chart.
|
||||
type AnnotationSeries struct {
|
||||
Name string
|
||||
|
@ -48,17 +53,17 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
|||
Right: 0,
|
||||
Bottom: 0,
|
||||
}
|
||||
if as.Style.IsZero() || as.Style.Show {
|
||||
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 = Math.MinInt(box.Top, ab.Top)
|
||||
box.Left = Math.MinInt(box.Left, ab.Left)
|
||||
box.Right = Math.MaxInt(box.Right, ab.Right)
|
||||
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
|
||||
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
|
||||
|
@ -66,7 +71,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
|||
|
||||
// Render draws the series.
|
||||
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||
if as.Style.IsZero() || as.Style.Show {
|
||||
if !as.Style.Hidden {
|
||||
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||
for _, a := range as.Annotations {
|
||||
style := a.Style.InheritFrom(seriesStyle)
|
||||
|
|
|
@ -4,17 +4,14 @@ import (
|
|||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
as := AnnotationSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
Annotations: []Value2{
|
||||
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||
|
@ -24,10 +21,10 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
|
|||
}
|
||||
|
||||
r, err := PNG(110, 110)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
xrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
|
@ -52,19 +49,18 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
|
|||
}
|
||||
|
||||
box := as.Measure(r, cb, xrange, yrange, sd)
|
||||
assert.False(box.IsZero())
|
||||
assert.Equal(-5.0, box.Top)
|
||||
assert.Equal(5.0, box.Left)
|
||||
assert.Equal(146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px.
|
||||
assert.Equal(115.0, box.Bottom)
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
as := AnnotationSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
FillColor: drawing.ColorWhite,
|
||||
StrokeColor: drawing.ColorBlack,
|
||||
},
|
||||
|
@ -77,10 +73,10 @@ func TestAnnotationSeriesRender(t *testing.T) {
|
|||
}
|
||||
|
||||
r, err := PNG(110, 110)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
xrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
|
@ -107,13 +103,13 @@ func TestAnnotationSeriesRender(t *testing.T) {
|
|||
as.Render(r, cb, xrange, yrange, sd)
|
||||
|
||||
rr, isRaster := r.(*rasterRenderer)
|
||||
assert.True(isRaster)
|
||||
assert.NotNil(rr)
|
||||
testutil.AssertTrue(t, isRaster)
|
||||
testutil.AssertNotNil(t, rr)
|
||||
|
||||
c := rr.i.At(38, 70)
|
||||
converted, isRGBA := color.RGBAModel.Convert(c).(color.RGBA)
|
||||
assert.True(isRGBA)
|
||||
assert.Equal(0, converted.R)
|
||||
assert.Equal(0, converted.G)
|
||||
assert.Equal(0, converted.B)
|
||||
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]
|
||||
}
|
104
bar_chart.go
|
@ -30,6 +30,9 @@ type BarChart struct {
|
|||
|
||||
BarSpacing int
|
||||
|
||||
UseBaseValue bool
|
||||
BaseValue float64
|
||||
|
||||
Font *truetype.Font
|
||||
defaultFont *truetype.Font
|
||||
|
||||
|
@ -125,7 +128,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
|
||||
yr = bc.setRangeDomains(canvasBox, yr)
|
||||
}
|
||||
|
||||
bc.drawCanvas(r, canvasBox)
|
||||
bc.drawBars(r, canvasBox, yr)
|
||||
bc.drawXAxis(r, canvasBox)
|
||||
bc.drawYAxis(r, canvasBox, yr, yt)
|
||||
|
@ -138,6 +141,10 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
return r.Save(w)
|
||||
}
|
||||
|
||||
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||
Draw.Box(r, canvasBox, bc.getCanvasStyle())
|
||||
}
|
||||
|
||||
func (bc BarChart) getRanges() Range {
|
||||
var yrange Range
|
||||
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
|
||||
|
@ -194,11 +201,20 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
|||
|
||||
by = canvasBox.Bottom - yr.Translate(bar.Value)
|
||||
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom,
|
||||
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)))
|
||||
|
@ -208,7 +224,7 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
|||
}
|
||||
|
||||
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
|
||||
if bc.XAxis.Show {
|
||||
if !bc.XAxis.Hidden {
|
||||
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||
axisStyle.WriteToRenderer(r)
|
||||
|
||||
|
@ -247,44 +263,44 @@ func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
|
|||
}
|
||||
|
||||
func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
|
||||
if bc.YAxis.Style.Show {
|
||||
axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes())
|
||||
axisStyle.WriteToRenderer(r)
|
||||
|
||||
r.MoveTo(canvasBox.Right, canvasBox.Top)
|
||||
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||
r.Stroke()
|
||||
|
||||
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
|
||||
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
|
||||
r.Stroke()
|
||||
|
||||
var ty int
|
||||
var tb Box
|
||||
for _, t := range ticks {
|
||||
ty = canvasBox.Bottom - yr.Translate(t.Value)
|
||||
|
||||
axisStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||
r.MoveTo(canvasBox.Right, ty)
|
||||
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
|
||||
r.Stroke()
|
||||
|
||||
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||
tb = r.MeasureText(t.Label)
|
||||
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
||||
}
|
||||
|
||||
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.Show {
|
||||
Draw.TextWithin(r, bc.Title, bc.box(), bc.styleDefaultsTitle())
|
||||
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.Show
|
||||
return !bc.YAxis.Style.Hidden
|
||||
}
|
||||
|
||||
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
|
||||
|
@ -304,7 +320,7 @@ func (bc BarChart) getValueFormatters() ValueFormatter {
|
|||
}
|
||||
|
||||
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
|
||||
if bc.YAxis.Style.Show {
|
||||
if !bc.YAxis.Style.Hidden {
|
||||
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
|
||||
}
|
||||
return
|
||||
|
@ -350,7 +366,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
|||
|
||||
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
|
||||
|
||||
if bc.XAxis.Show {
|
||||
if !bc.XAxis.Hidden {
|
||||
xaxisHeight := DefaultVerticalTickHeight
|
||||
|
||||
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||
|
@ -368,7 +384,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
|||
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||
|
||||
xaxisHeight = Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,7 +398,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
|||
axesOuterBox = axesOuterBox.Grow(xbox)
|
||||
}
|
||||
|
||||
if bc.YAxis.Style.Show {
|
||||
if !bc.YAxis.Style.Hidden {
|
||||
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
|
||||
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||
}
|
||||
|
@ -396,8 +412,8 @@ func (bc BarChart) box() Box {
|
|||
dpb := bc.Background.Padding.GetBottom(50)
|
||||
|
||||
return Box{
|
||||
Top: 20,
|
||||
Left: 20,
|
||||
Top: bc.Background.Padding.GetTop(20),
|
||||
Left: bc.Background.Padding.GetLeft(20),
|
||||
Right: bc.GetWidth() - dpr,
|
||||
Bottom: bc.GetHeight() - dpb,
|
||||
}
|
||||
|
@ -435,7 +451,7 @@ func (bc BarChart) styleDefaultsTitle() Style {
|
|||
}
|
||||
|
||||
func (bc BarChart) getTitleFontSize() float64 {
|
||||
effectiveDimension := Math.MinInt(bc.GetWidth(), bc.GetHeight())
|
||||
effectiveDimension := MinInt(bc.GetWidth(), bc.GetHeight())
|
||||
if effectiveDimension >= 2048 {
|
||||
return 48
|
||||
} else if effectiveDimension >= 1024 {
|
||||
|
|
|
@ -5,20 +5,15 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestBarChartRender(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
TitleStyle: StyleShow(),
|
||||
XAxis: StyleShow(),
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
Bars: []Value{
|
||||
{Value: 1.0, Label: "One"},
|
||||
{Value: 2.0, Label: "Two"},
|
||||
|
@ -30,21 +25,16 @@ func TestBarChartRender(t *testing.T) {
|
|||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := bc.Render(PNG, buf)
|
||||
assert.Nil(err)
|
||||
assert.NotZero(buf.Len())
|
||||
testutil.AssertNil(t, err)
|
||||
testutil.AssertNotZero(t, buf.Len())
|
||||
}
|
||||
|
||||
func TestBarChartRenderZero(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
TitleStyle: StyleShow(),
|
||||
XAxis: StyleShow(),
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
Bars: []Value{
|
||||
{Value: 0.0, Label: "One"},
|
||||
{Value: 0.0, Label: "Two"},
|
||||
|
@ -53,64 +43,64 @@ func TestBarChartRenderZero(t *testing.T) {
|
|||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := bc.Render(PNG, buf)
|
||||
assert.NotNil(err)
|
||||
testutil.AssertNotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBarChartProps(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
|
||||
assert.Equal(DefaultDPI, bc.GetDPI())
|
||||
testutil.AssertEqual(t, DefaultDPI, bc.GetDPI())
|
||||
bc.DPI = 100
|
||||
assert.Equal(100, bc.GetDPI())
|
||||
testutil.AssertEqual(t, 100, bc.GetDPI())
|
||||
|
||||
assert.Nil(bc.GetFont())
|
||||
testutil.AssertNil(t, bc.GetFont())
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
bc.Font = f
|
||||
assert.NotNil(bc.GetFont())
|
||||
testutil.AssertNotNil(t, bc.GetFont())
|
||||
|
||||
assert.Equal(DefaultChartWidth, bc.GetWidth())
|
||||
testutil.AssertEqual(t, DefaultChartWidth, bc.GetWidth())
|
||||
bc.Width = DefaultChartWidth - 1
|
||||
assert.Equal(DefaultChartWidth-1, bc.GetWidth())
|
||||
testutil.AssertEqual(t, DefaultChartWidth-1, bc.GetWidth())
|
||||
|
||||
assert.Equal(DefaultChartHeight, bc.GetHeight())
|
||||
testutil.AssertEqual(t, DefaultChartHeight, bc.GetHeight())
|
||||
bc.Height = DefaultChartHeight - 1
|
||||
assert.Equal(DefaultChartHeight-1, bc.GetHeight())
|
||||
testutil.AssertEqual(t, DefaultChartHeight-1, bc.GetHeight())
|
||||
|
||||
assert.Equal(DefaultBarSpacing, bc.GetBarSpacing())
|
||||
testutil.AssertEqual(t, DefaultBarSpacing, bc.GetBarSpacing())
|
||||
bc.BarSpacing = 150
|
||||
assert.Equal(150, bc.GetBarSpacing())
|
||||
testutil.AssertEqual(t, 150, bc.GetBarSpacing())
|
||||
|
||||
assert.Equal(DefaultBarWidth, bc.GetBarWidth())
|
||||
testutil.AssertEqual(t, DefaultBarWidth, bc.GetBarWidth())
|
||||
bc.BarWidth = 75
|
||||
assert.Equal(75, bc.GetBarWidth())
|
||||
testutil.AssertEqual(t, 75, bc.GetBarWidth())
|
||||
}
|
||||
|
||||
func TestBarChartRenderNoBars(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
|
||||
assert.NotNil(err)
|
||||
testutil.AssertNotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBarChartGetRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
testutil.AssertNotNil(t, yr)
|
||||
testutil.AssertFalse(t, yr.IsZero())
|
||||
|
||||
assert.Equal(-math.MaxFloat64, yr.GetMax())
|
||||
assert.Equal(math.MaxFloat64, yr.GetMin())
|
||||
testutil.AssertEqual(t, -math.MaxFloat64, yr.GetMax())
|
||||
testutil.AssertEqual(t, math.MaxFloat64, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesBarsMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Bars: []Value{
|
||||
|
@ -120,15 +110,15 @@ func TestBarChartGetRangesBarsMinMax(t *testing.T) {
|
|||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
testutil.AssertNotNil(t, yr)
|
||||
testutil.AssertFalse(t, yr.IsZero())
|
||||
|
||||
assert.Equal(10, yr.GetMax())
|
||||
assert.Equal(1, yr.GetMin())
|
||||
testutil.AssertEqual(t, 10, yr.GetMax())
|
||||
testutil.AssertEqual(t, 1, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
YAxis: YAxis{
|
||||
|
@ -148,15 +138,15 @@ func TestBarChartGetRangesMinMax(t *testing.T) {
|
|||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
testutil.AssertNotNil(t, yr)
|
||||
testutil.AssertFalse(t, yr.IsZero())
|
||||
|
||||
assert.Equal(15, yr.GetMax())
|
||||
assert.Equal(5, yr.GetMin())
|
||||
testutil.AssertEqual(t, 15, yr.GetMax())
|
||||
testutil.AssertEqual(t, 5, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesTicksMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
YAxis: YAxis{
|
||||
|
@ -172,57 +162,56 @@ func TestBarChartGetRangesTicksMinMax(t *testing.T) {
|
|||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
testutil.AssertNotNil(t, yr)
|
||||
testutil.AssertFalse(t, yr.IsZero())
|
||||
|
||||
assert.Equal(11, yr.GetMax())
|
||||
assert.Equal(7, yr.GetMin())
|
||||
testutil.AssertEqual(t, 11, yr.GetMax())
|
||||
testutil.AssertEqual(t, 7, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartHasAxes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
assert.False(bc.hasAxes())
|
||||
testutil.AssertTrue(t, bc.hasAxes())
|
||||
bc.YAxis = YAxis{
|
||||
Style: StyleShow(),
|
||||
Style: Hidden(),
|
||||
}
|
||||
|
||||
assert.True(bc.hasAxes())
|
||||
testutil.AssertFalse(t, bc.hasAxes())
|
||||
}
|
||||
|
||||
func TestBarChartGetDefaultCanvasBox(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
b := bc.getDefaultCanvasBox()
|
||||
assert.False(b.IsZero())
|
||||
testutil.AssertFalse(t, b.IsZero())
|
||||
}
|
||||
|
||||
func TestBarChartSetRangeDomains(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
cb := bc.box()
|
||||
yr := bc.getRanges()
|
||||
yr2 := bc.setRangeDomains(cb, yr)
|
||||
assert.NotZero(yr2.GetDomain())
|
||||
testutil.AssertNotZero(t, yr2.GetDomain())
|
||||
}
|
||||
|
||||
func TestBarChartGetValueFormatters(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{}
|
||||
vf := bc.getValueFormatters()
|
||||
assert.NotNil(vf)
|
||||
assert.Equal("1234.00", vf(1234.0))
|
||||
testutil.AssertNotNil(t, vf)
|
||||
testutil.AssertEqual(t, "1234.00", vf(1234.0))
|
||||
|
||||
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
|
||||
assert.Equal("test", bc.getValueFormatters()(1234))
|
||||
testutil.AssertEqual(t, "test", bc.getValueFormatters()(1234))
|
||||
}
|
||||
|
||||
func TestBarChartGetAxesTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Bars: []Value{
|
||||
|
@ -233,20 +222,21 @@ func TestBarChartGetAxesTicks(t *testing.T) {
|
|||
}
|
||||
|
||||
r, err := PNG(128, 128)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
yr := bc.getRanges()
|
||||
yf := bc.getValueFormatters()
|
||||
|
||||
bc.YAxis.Style.Hidden = true
|
||||
ticks := bc.getAxesTicks(r, yr, yf)
|
||||
assert.Empty(ticks)
|
||||
testutil.AssertEmpty(t, ticks)
|
||||
|
||||
bc.YAxis.Style.Show = true
|
||||
bc.YAxis.Style.Hidden = false
|
||||
ticks = bc.getAxesTicks(r, yr, yf)
|
||||
assert.Len(ticks, 2)
|
||||
testutil.AssertLen(t, ticks, 2)
|
||||
}
|
||||
|
||||
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
|
@ -261,15 +251,15 @@ func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
|
|||
}
|
||||
|
||||
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.NotZero(spacing)
|
||||
testutil.AssertNotZero(t, spacing)
|
||||
|
||||
bc.BarWidth = 250
|
||||
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.Zero(spacing)
|
||||
testutil.AssertZero(t, spacing)
|
||||
}
|
||||
|
||||
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
|
@ -286,35 +276,35 @@ func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
|
|||
cb := bc.box()
|
||||
|
||||
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.NotZero(spacing)
|
||||
testutil.AssertNotZero(t, spacing)
|
||||
|
||||
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||
assert.Equal(10, barWidth)
|
||||
testutil.AssertEqual(t, 10, barWidth)
|
||||
|
||||
bc.BarWidth = 250
|
||||
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.Zero(spacing)
|
||||
testutil.AssertZero(t, spacing)
|
||||
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||
assert.Equal(199, barWidth)
|
||||
testutil.AssertEqual(t, 199, barWidth)
|
||||
|
||||
assert.Equal(cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
|
||||
testutil.AssertEqual(t, cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
|
||||
|
||||
bw, bs, total := bc.calculateScaledTotalWidth(cb)
|
||||
assert.Equal(spacing, bs)
|
||||
assert.Equal(barWidth, bw)
|
||||
assert.Equal(cb.Width()+1, total)
|
||||
testutil.AssertEqual(t, spacing, bs)
|
||||
testutil.AssertEqual(t, barWidth, bw)
|
||||
testutil.AssertEqual(t, cb.Width()+1, total)
|
||||
}
|
||||
|
||||
func TestBarChatGetTitleFontSize(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
|
||||
assert.Equal(48, size)
|
||||
testutil.AssertEqual(t, 48, size)
|
||||
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
|
||||
assert.Equal(24, size)
|
||||
testutil.AssertEqual(t, 24, size)
|
||||
size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
|
||||
assert.Equal(18, size)
|
||||
testutil.AssertEqual(t, 18, size)
|
||||
size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
|
||||
assert.Equal(12, size)
|
||||
testutil.AssertEqual(t, 12, size)
|
||||
size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
|
||||
assert.Equal(10, size)
|
||||
testutil.AssertEqual(t, 10, size)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ package chart
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*BollingerBandsSeries)(nil)
|
||||
)
|
||||
|
||||
// BollingerBandsSeries draws bollinger bands for an inner series.
|
||||
|
@ -14,9 +18,9 @@ type BollingerBandsSeries struct {
|
|||
|
||||
Period int
|
||||
K float64
|
||||
InnerSeries ValueProvider
|
||||
InnerSeries ValuesProvider
|
||||
|
||||
valueBuffer *RingBuffer
|
||||
valueBuffer *ValueBuffer
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
|
@ -42,7 +46,9 @@ func (bbs BollingerBandsSeries) GetPeriod() int {
|
|||
return bbs.Period
|
||||
}
|
||||
|
||||
// GetK returns the K value.
|
||||
// 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 {
|
||||
|
@ -54,35 +60,35 @@ func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
|
|||
}
|
||||
|
||||
// Len returns the number of elements in the series.
|
||||
func (bbs *BollingerBandsSeries) Len() int {
|
||||
func (bbs BollingerBandsSeries) Len() int {
|
||||
return bbs.InnerSeries.Len()
|
||||
}
|
||||
|
||||
// GetBoundedValue gets the bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedValue(index int) (x, y1, y2 float64) {
|
||||
// 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 = NewRingBufferWithCapacity(bbs.GetPeriod())
|
||||
bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
|
||||
}
|
||||
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
|
||||
bbs.valueBuffer.Dequeue()
|
||||
}
|
||||
px, py := bbs.InnerSeries.GetValue(index)
|
||||
px, py := bbs.InnerSeries.GetValues(index)
|
||||
bbs.valueBuffer.Enqueue(py)
|
||||
x = px
|
||||
|
||||
ay := bbs.getAverage(bbs.valueBuffer)
|
||||
std := bbs.getStdDev(bbs.valueBuffer)
|
||||
ay := Seq{bbs.valueBuffer}.Average()
|
||||
std := Seq{bbs.valueBuffer}.StdDev()
|
||||
|
||||
y1 = ay + (bbs.GetK() * std)
|
||||
y2 = ay - (bbs.GetK() * std)
|
||||
return
|
||||
}
|
||||
|
||||
// GetBoundedLastValue returns the last bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) {
|
||||
// GetBoundedLastValues returns the last bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
|
||||
if bbs.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
|
@ -93,15 +99,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) {
|
|||
startAt = 0
|
||||
}
|
||||
|
||||
vb := NewRingBufferWithCapacity(period)
|
||||
vb := NewValueBufferWithCapacity(period)
|
||||
for index := startAt; index < seriesLength; index++ {
|
||||
xn, yn := bbs.InnerSeries.GetValue(index)
|
||||
xn, yn := bbs.InnerSeries.GetValues(index)
|
||||
vb.Enqueue(yn)
|
||||
x = xn
|
||||
}
|
||||
|
||||
ay := bbs.getAverage(vb)
|
||||
std := bbs.getStdDev(vb)
|
||||
ay := Seq{vb}.Average()
|
||||
std := Seq{vb}.StdDev()
|
||||
|
||||
y1 = ay + (bbs.GetK() * std)
|
||||
y2 = ay - (bbs.GetK() * std)
|
||||
|
@ -120,37 +126,6 @@ func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrang
|
|||
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {
|
||||
var accum float64
|
||||
valueBuffer.Each(func(v interface{}) {
|
||||
if typed, isTyped := v.(float64); isTyped {
|
||||
accum += typed
|
||||
}
|
||||
})
|
||||
return accum / float64(valueBuffer.Len())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 {
|
||||
if valueBuffer.Len() == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var variance float64
|
||||
m := bbs.getAverage(valueBuffer)
|
||||
|
||||
valueBuffer.Each(func(v interface{}) {
|
||||
if n, isTyped := v.(float64); isTyped {
|
||||
variance += (float64(n) - m) * (float64(n) - m)
|
||||
}
|
||||
})
|
||||
|
||||
return variance / float64(valueBuffer.Len())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getStdDev(valueBuffer *RingBuffer) float64 {
|
||||
return math.Pow(bbs.getVariance(valueBuffer), 0.5)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (bbs BollingerBandsSeries) Validate() error {
|
||||
if bbs.InnerSeries == nil {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestBollingerBandSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Random(100, 1024),
|
||||
s1 := mockValuesProvider{
|
||||
X: LinearRange(1.0, 100.0),
|
||||
Y: RandomValuesWithMax(100, 1024),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
|
@ -24,28 +25,28 @@ func TestBollingerBandSeries(t *testing.T) {
|
|||
y2values := make([]float64, 100)
|
||||
|
||||
for x := 0; x < 100; x++ {
|
||||
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValue(x)
|
||||
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x)
|
||||
}
|
||||
|
||||
for x := bbs.GetPeriod(); x < 100; x++ {
|
||||
assert.True(y1values[x] > y2values[x])
|
||||
testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBollingerBandLastValue(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Float64(1.0, 100.0),
|
||||
s1 := mockValuesProvider{
|
||||
X: LinearRange(1.0, 100.0),
|
||||
Y: LinearRange(1.0, 100.0),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
InnerSeries: s1,
|
||||
}
|
||||
|
||||
x, y1, y2 := bbs.GetBoundedLastValue()
|
||||
assert.Equal(100.0, x)
|
||||
assert.Equal(101, math.Floor(y1))
|
||||
assert.Equal(83, math.Floor(y2))
|
||||
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},
|
||||
},
|
||||
}
|
||||
}
|
70
box.go
|
@ -89,12 +89,12 @@ func (b Box) GetBottom(defaults ...int) int {
|
|||
|
||||
// Width returns the width
|
||||
func (b Box) Width() int {
|
||||
return Math.AbsInt(b.Right - b.Left)
|
||||
return AbsInt(b.Right - b.Left)
|
||||
}
|
||||
|
||||
// Height returns the height
|
||||
func (b Box) Height() int {
|
||||
return Math.AbsInt(b.Bottom - b.Top)
|
||||
return AbsInt(b.Bottom - b.Top)
|
||||
}
|
||||
|
||||
// Center returns the center of the box
|
||||
|
@ -146,10 +146,10 @@ func (b Box) Equals(other Box) bool {
|
|||
// Grow grows a box based on another box.
|
||||
func (b Box) Grow(other Box) Box {
|
||||
return Box{
|
||||
Top: Math.MinInt(b.Top, other.Top),
|
||||
Left: Math.MinInt(b.Left, other.Left),
|
||||
Right: Math.MaxInt(b.Right, other.Right),
|
||||
Bottom: Math.MaxInt(b.Bottom, other.Bottom),
|
||||
Top: MinInt(b.Top, other.Top),
|
||||
Left: MinInt(b.Left, other.Left),
|
||||
Right: MaxInt(b.Right, other.Right),
|
||||
Bottom: MaxInt(b.Bottom, other.Bottom),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,10 +220,10 @@ func (b Box) Fit(other Box) Box {
|
|||
func (b Box) Constrain(other Box) Box {
|
||||
newBox := b.Clone()
|
||||
|
||||
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
|
||||
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
|
||||
newBox.Right = Math.MinInt(newBox.Right, other.Right)
|
||||
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
|
||||
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
|
||||
}
|
||||
|
@ -254,6 +254,22 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
|
|||
return newBox
|
||||
}
|
||||
|
||||
func (b Box) Validate() error {
|
||||
if b.Left < 0 {
|
||||
return fmt.Errorf("invalid left; must be >= 0")
|
||||
}
|
||||
if b.Right < 0 {
|
||||
return fmt.Errorf("invalid right; must be > 0")
|
||||
}
|
||||
if b.Top < 0 {
|
||||
return fmt.Errorf("invalid top; must be > 0")
|
||||
}
|
||||
if b.Bottom < 0 {
|
||||
return fmt.Errorf("invalid bottom; must be > 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BoxCorners is a box with independent corners.
|
||||
type BoxCorners struct {
|
||||
TopLeft, TopRight, BottomRight, BottomLeft Point
|
||||
|
@ -262,36 +278,36 @@ type BoxCorners struct {
|
|||
// Box return the BoxCorners as a regular box.
|
||||
func (bc BoxCorners) Box() Box {
|
||||
return Box{
|
||||
Top: Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
||||
Left: Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
||||
Right: Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
||||
Bottom: Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
||||
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 := Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
maxRight := Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
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 := Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
maxBottom := Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
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 := Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
right := Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
left := MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
right := MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
x = ((right - left) >> 1) + left
|
||||
|
||||
top := Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
bottom := Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
y = ((bottom - top) >> 1) + top
|
||||
|
||||
return
|
||||
|
@ -301,12 +317,12 @@ func (bc BoxCorners) Center() (x, y int) {
|
|||
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
|
||||
cx, cy := bc.Center()
|
||||
|
||||
thetaRadians := Math.DegreesToRadians(thetaDegrees)
|
||||
thetaRadians := DegreesToRadians(thetaDegrees)
|
||||
|
||||
tlx, tly := Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
||||
trx, try := Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
||||
brx, bry := Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
||||
blx, bly := Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
|
||||
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},
|
||||
|
|
134
box_test.go
|
@ -4,131 +4,131 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestBoxClone(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
|
||||
b := a.Clone()
|
||||
assert.True(a.Equals(b))
|
||||
assert.True(b.Equals(a))
|
||||
testutil.AssertTrue(t, a.Equals(b))
|
||||
testutil.AssertTrue(t, b.Equals(a))
|
||||
}
|
||||
|
||||
func TestBoxEquals(t *testing.T) {
|
||||
assert := assert.New(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}
|
||||
assert.True(a.Equals(a))
|
||||
assert.True(a.Equals(c))
|
||||
assert.True(c.Equals(a))
|
||||
assert.False(a.Equals(b))
|
||||
assert.False(c.Equals(b))
|
||||
assert.False(b.Equals(a))
|
||||
assert.False(b.Equals(c))
|
||||
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) {
|
||||
assert := assert.New(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
|
||||
assert.True(a.IsBiggerThan(b))
|
||||
assert.False(a.IsBiggerThan(c))
|
||||
assert.True(c.IsBiggerThan(a))
|
||||
testutil.AssertTrue(t, a.IsBiggerThan(b))
|
||||
testutil.AssertFalse(t, a.IsBiggerThan(c))
|
||||
testutil.AssertTrue(t, c.IsBiggerThan(a))
|
||||
}
|
||||
|
||||
func TestBoxIsSmallerThan(t *testing.T) {
|
||||
assert := assert.New(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
|
||||
assert.False(a.IsSmallerThan(b))
|
||||
assert.True(a.IsSmallerThan(c))
|
||||
assert.False(c.IsSmallerThan(a))
|
||||
testutil.AssertFalse(t, a.IsSmallerThan(b))
|
||||
testutil.AssertTrue(t, a.IsSmallerThan(c))
|
||||
testutil.AssertFalse(t, c.IsSmallerThan(a))
|
||||
}
|
||||
|
||||
func TestBoxGrow(t *testing.T) {
|
||||
assert := assert.New(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)
|
||||
assert.False(c.Equals(b))
|
||||
assert.False(c.Equals(a))
|
||||
assert.Equal(1, c.Top)
|
||||
assert.Equal(2, c.Left)
|
||||
assert.Equal(30, c.Right)
|
||||
assert.Equal(35, c.Bottom)
|
||||
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) {
|
||||
assert := assert.New(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)
|
||||
assert.Equal(a.Left, fab.Left)
|
||||
assert.Equal(a.Right, fab.Right)
|
||||
assert.True(fab.Top < fab.Bottom)
|
||||
assert.True(fab.Left < fab.Right)
|
||||
assert.True(math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
|
||||
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)
|
||||
assert.Equal(a.Top, fac.Top)
|
||||
assert.Equal(a.Bottom, fac.Bottom)
|
||||
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
|
||||
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) {
|
||||
assert := assert.New(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)
|
||||
assert.Equal(64, cab.Top)
|
||||
assert.Equal(64, cab.Left)
|
||||
assert.Equal(192, cab.Right)
|
||||
assert.Equal(170, cab.Bottom)
|
||||
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)
|
||||
assert.Equal(64, cac.Top)
|
||||
assert.Equal(64, cac.Left)
|
||||
assert.Equal(170, cac.Right)
|
||||
assert.Equal(192, cac.Bottom)
|
||||
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) {
|
||||
assert := assert.New(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)
|
||||
assert.Equal(15, c.Top, c.String())
|
||||
assert.Equal(5, c.Left, c.String())
|
||||
assert.Equal(95, c.Right, c.String())
|
||||
assert.Equal(95, c.Bottom, c.String())
|
||||
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)
|
||||
assert.Equal(5, d.Top, d.String())
|
||||
assert.Equal(5, d.Left, d.String())
|
||||
assert.Equal(85, d.Right, d.String())
|
||||
assert.Equal(95, d.Bottom, d.String())
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
b := Box{
|
||||
Top: 5,
|
||||
|
@ -138,14 +138,14 @@ func TestBoxShift(t *testing.T) {
|
|||
}
|
||||
|
||||
shifted := b.Shift(1, 2)
|
||||
assert.Equal(7, shifted.Top)
|
||||
assert.Equal(6, shifted.Left)
|
||||
assert.Equal(11, shifted.Right)
|
||||
assert.Equal(12, shifted.Bottom)
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
b := Box{
|
||||
Top: 10,
|
||||
|
@ -154,12 +154,12 @@ func TestBoxCenter(t *testing.T) {
|
|||
Bottom: 30,
|
||||
}
|
||||
cx, cy := b.Center()
|
||||
assert.Equal(15, cx)
|
||||
assert.Equal(20, cy)
|
||||
testutil.AssertEqual(t, 15, cx)
|
||||
testutil.AssertEqual(t, 20, cy)
|
||||
}
|
||||
|
||||
func TestBoxCornersCenter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BoxCorners{
|
||||
TopLeft: Point{5, 5},
|
||||
|
@ -169,12 +169,12 @@ func TestBoxCornersCenter(t *testing.T) {
|
|||
}
|
||||
|
||||
cx, cy := bc.Center()
|
||||
assert.Equal(10, cx)
|
||||
assert.Equal(10, cy)
|
||||
testutil.AssertEqual(t, 10, cx)
|
||||
testutil.AssertEqual(t, 10, cy)
|
||||
}
|
||||
|
||||
func TestBoxCornersRotate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
bc := BoxCorners{
|
||||
TopLeft: Point{5, 5},
|
||||
|
@ -184,5 +184,5 @@ func TestBoxCornersRotate(t *testing.T) {
|
|||
}
|
||||
|
||||
rotated := bc.Rotate(45)
|
||||
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
||||
testutil.AssertTrue(t, rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
||||
}
|
||||
|
|
85
chart.go
|
@ -32,6 +32,8 @@ type Chart struct {
|
|||
|
||||
Series []Series
|
||||
Elements []Renderable
|
||||
|
||||
Log Logger
|
||||
}
|
||||
|
||||
// GetDPI returns the dpi for the chart.
|
||||
|
@ -74,8 +76,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
if len(c.Series) == 0 {
|
||||
return errors.New("please provide at least one series")
|
||||
}
|
||||
if visibleSeriesErr := c.checkHasVisibleSeries(); visibleSeriesErr != nil {
|
||||
return visibleSeriesErr
|
||||
if err := c.checkHasVisibleSeries(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.YAxisSecondary.AxisType = YAxisSecondary
|
||||
|
@ -101,6 +103,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox := c.getDefaultCanvasBox()
|
||||
xf, yf, yfa := c.getValueFormatters()
|
||||
|
||||
Debugf(c.Log, "chart; canvas box: %v", canvasBox)
|
||||
|
||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||
|
||||
err = c.checkRanges(xr, yr, yra)
|
||||
|
@ -114,6 +118,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||
|
||||
Debugf(c.Log, "chart; axes adjusted canvas box: %v", canvasBox)
|
||||
|
||||
// do a second pass in case things haven't settled yet.
|
||||
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||
|
@ -124,6 +130,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa)
|
||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||
|
||||
Debugf(c.Log, "chart; annotation adjusted canvas box: %v", canvasBox)
|
||||
}
|
||||
|
||||
c.drawCanvas(r, canvasBox)
|
||||
|
@ -142,16 +150,14 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
}
|
||||
|
||||
func (c Chart) checkHasVisibleSeries() error {
|
||||
hasVisibleSeries := false
|
||||
var style Style
|
||||
for _, s := range c.Series {
|
||||
style = s.GetStyle()
|
||||
hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show)
|
||||
if !style.Hidden {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !hasVisibleSeries {
|
||||
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
|
||||
}
|
||||
return nil
|
||||
return fmt.Errorf("chart render; must have (1) visible series")
|
||||
}
|
||||
|
||||
func (c Chart) validateSeries() error {
|
||||
|
@ -175,12 +181,12 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
// note: a possible future optimization is to not scan the series values if
|
||||
// all axis are represented by either custom ticks or custom ranges.
|
||||
for _, s := range c.Series {
|
||||
if s.GetStyle().IsZero() || s.GetStyle().Show {
|
||||
if !s.GetStyle().Hidden {
|
||||
seriesAxis := s.GetYAxis()
|
||||
if bvp, isBoundedValueProvider := s.(BoundedValueProvider); isBoundedValueProvider {
|
||||
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
|
||||
seriesLength := bvp.Len()
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy1, vy2 := bvp.GetBoundedValue(index)
|
||||
vx, vy1, vy2 := bvp.GetBoundedValues(index)
|
||||
|
||||
minx = math.Min(minx, vx)
|
||||
maxx = math.Max(maxx, vx)
|
||||
|
@ -198,10 +204,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
seriesMappedToSecondaryAxis = true
|
||||
}
|
||||
}
|
||||
} else if vp, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
seriesLength := vp.Len()
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy := vp.GetValue(index)
|
||||
vx, vy := vp.GetValues(index)
|
||||
|
||||
minx = math.Min(minx, vx)
|
||||
maxx = math.Max(maxx, vx)
|
||||
|
@ -262,11 +268,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
yrange.SetMin(miny)
|
||||
yrange.SetMax(maxy)
|
||||
|
||||
// only round if we're showing the axis
|
||||
if c.YAxis.Style.Show {
|
||||
if !c.YAxis.Style.Hidden {
|
||||
delta := yrange.GetDelta()
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
|
||||
roundTo := GetRoundToForDelta(delta)
|
||||
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
|
||||
|
||||
yrange.SetMin(rmin)
|
||||
yrange.SetMax(rmax)
|
||||
|
@ -285,10 +290,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
yrangeAlt.SetMin(minya)
|
||||
yrangeAlt.SetMax(maxya)
|
||||
|
||||
if c.YAxisSecondary.Style.Show {
|
||||
if !c.YAxisSecondary.Style.Hidden {
|
||||
delta := yrangeAlt.GetDelta()
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
roundTo := GetRoundToForDelta(delta)
|
||||
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
yrangeAlt.SetMin(rmin)
|
||||
yrangeAlt.SetMax(rmax)
|
||||
}
|
||||
|
@ -298,6 +303,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
}
|
||||
|
||||
func (c Chart) checkRanges(xr, yr, yra Range) error {
|
||||
Debugf(c.Log, "checking xrange: %v", xr)
|
||||
xDelta := xr.GetDelta()
|
||||
if math.IsInf(xDelta, 0) {
|
||||
return errors.New("infinite x-range delta")
|
||||
|
@ -309,6 +315,7 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
|
|||
return errors.New("zero x-range delta; there needs to be at least (2) values")
|
||||
}
|
||||
|
||||
Debugf(c.Log, "checking yrange: %v", yr)
|
||||
yDelta := yr.GetDelta()
|
||||
if math.IsInf(yDelta, 0) {
|
||||
return errors.New("infinite y-range delta")
|
||||
|
@ -316,11 +323,9 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
|
|||
if math.IsNaN(yDelta) {
|
||||
return errors.New("nan y-range delta")
|
||||
}
|
||||
if yDelta == 0 {
|
||||
return errors.New("zero y-range delta")
|
||||
}
|
||||
|
||||
if c.hasSecondarySeries() {
|
||||
Debugf(c.Log, "checking secondary yrange: %v", yra)
|
||||
yraDelta := yra.GetDelta()
|
||||
if math.IsInf(yraDelta, 0) {
|
||||
return errors.New("infinite secondary y-range delta")
|
||||
|
@ -328,9 +333,6 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
|
|||
if math.IsNaN(yraDelta) {
|
||||
return errors.New("nan secondary y-range delta")
|
||||
}
|
||||
if yraDelta == 0 {
|
||||
return errors.New("zero secondary y-range delta")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -366,17 +368,17 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
|
|||
}
|
||||
|
||||
func (c Chart) hasAxes() bool {
|
||||
return c.XAxis.Style.Show || c.YAxis.Style.Show || c.YAxisSecondary.Style.Show
|
||||
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.Show {
|
||||
if !c.XAxis.Style.Hidden {
|
||||
xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf)
|
||||
}
|
||||
if c.YAxis.Style.Show {
|
||||
if !c.YAxis.Style.Hidden {
|
||||
yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
|
||||
}
|
||||
if c.YAxisSecondary.Style.Show {
|
||||
if !c.YAxisSecondary.Style.Hidden {
|
||||
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
|
||||
}
|
||||
return
|
||||
|
@ -384,16 +386,19 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
|
|||
|
||||
func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
|
||||
axesOuterBox := canvasBox.Clone()
|
||||
if c.XAxis.Style.Show {
|
||||
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.Show {
|
||||
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.Show {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -410,7 +415,7 @@ func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range,
|
|||
func (c Chart) hasAnnotationSeries() bool {
|
||||
for _, s := range c.Series {
|
||||
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||
if as.Style.IsZero() || as.Style.Show {
|
||||
if !as.GetStyle().Hidden {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -431,7 +436,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
|
|||
annotationSeriesBox := canvasBox.Clone()
|
||||
for seriesIndex, s := range c.Series {
|
||||
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||
if as.Style.IsZero() || as.Style.Show {
|
||||
if !as.GetStyle().Hidden {
|
||||
style := c.styleDefaultsSeries(seriesIndex)
|
||||
var annotationBounds Box
|
||||
if as.YAxis == YAxisPrimary {
|
||||
|
@ -468,19 +473,19 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
|||
}
|
||||
|
||||
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
|
||||
if c.XAxis.Style.Show {
|
||||
if !c.XAxis.Style.Hidden {
|
||||
c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
|
||||
}
|
||||
if c.YAxis.Style.Show {
|
||||
if !c.YAxis.Style.Hidden {
|
||||
c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
|
||||
}
|
||||
if c.YAxisSecondary.Style.Show {
|
||||
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().IsZero() || s.GetStyle().Show {
|
||||
if !s.GetStyle().Hidden {
|
||||
if s.GetYAxis() == YAxisPrimary {
|
||||
s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
|
||||
} else if s.GetYAxis() == YAxisSecondary {
|
||||
|
@ -490,7 +495,7 @@ func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt R
|
|||
}
|
||||
|
||||
func (c Chart) drawTitle(r Renderer) {
|
||||
if len(c.Title) > 0 && c.TitleStyle.Show {
|
||||
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)
|
||||
|
|
343
chart_test.go
|
@ -8,57 +8,57 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestChartGetDPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultDPI, unset.GetDPI())
|
||||
assert.Equal(192, unset.GetDPI(192))
|
||||
testutil.AssertEqual(t, DefaultDPI, unset.GetDPI())
|
||||
testutil.AssertEqual(t, 192, unset.GetDPI(192))
|
||||
|
||||
set := Chart{DPI: 128}
|
||||
assert.Equal(128, set.GetDPI())
|
||||
assert.Equal(128, set.GetDPI(192))
|
||||
testutil.AssertEqual(t, 128, set.GetDPI())
|
||||
testutil.AssertEqual(t, 128, set.GetDPI(192))
|
||||
}
|
||||
|
||||
func TestChartGetFont(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
unset := Chart{}
|
||||
assert.Nil(unset.GetFont())
|
||||
testutil.AssertNil(t, unset.GetFont())
|
||||
|
||||
set := Chart{Font: f}
|
||||
assert.NotNil(set.GetFont())
|
||||
testutil.AssertNotNil(t, set.GetFont())
|
||||
}
|
||||
|
||||
func TestChartGetWidth(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultChartWidth, unset.GetWidth())
|
||||
testutil.AssertEqual(t, DefaultChartWidth, unset.GetWidth())
|
||||
|
||||
set := Chart{Width: DefaultChartWidth + 10}
|
||||
assert.Equal(DefaultChartWidth+10, set.GetWidth())
|
||||
testutil.AssertEqual(t, DefaultChartWidth+10, set.GetWidth())
|
||||
}
|
||||
|
||||
func TestChartGetHeight(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultChartHeight, unset.GetHeight())
|
||||
testutil.AssertEqual(t, DefaultChartHeight, unset.GetHeight())
|
||||
|
||||
set := Chart{Height: DefaultChartHeight + 10}
|
||||
assert.Equal(DefaultChartHeight+10, set.GetHeight())
|
||||
testutil.AssertEqual(t, DefaultChartHeight+10, set.GetHeight())
|
||||
}
|
||||
|
||||
func TestChartGetRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
|
@ -79,14 +79,14 @@ func TestChartGetRanges(t *testing.T) {
|
|||
}
|
||||
|
||||
xrange, yrange, yrangeAlt := c.getRanges()
|
||||
assert.Equal(-2.0, xrange.GetMin())
|
||||
assert.Equal(5.0, xrange.GetMax())
|
||||
testutil.AssertEqual(t, -2.0, xrange.GetMin())
|
||||
testutil.AssertEqual(t, 5.0, xrange.GetMax())
|
||||
|
||||
assert.Equal(-2.1, yrange.GetMin())
|
||||
assert.Equal(4.5, yrange.GetMax())
|
||||
testutil.AssertEqual(t, -2.1, yrange.GetMin())
|
||||
testutil.AssertEqual(t, 4.5, yrange.GetMax())
|
||||
|
||||
assert.Equal(10.0, yrangeAlt.GetMin())
|
||||
assert.Equal(14.0, yrangeAlt.GetMax())
|
||||
testutil.AssertEqual(t, 10.0, yrangeAlt.GetMin())
|
||||
testutil.AssertEqual(t, 14.0, yrangeAlt.GetMax())
|
||||
|
||||
cSet := Chart{
|
||||
XAxis: XAxis{
|
||||
|
@ -116,18 +116,18 @@ func TestChartGetRanges(t *testing.T) {
|
|||
}
|
||||
|
||||
xr2, yr2, yra2 := cSet.getRanges()
|
||||
assert.Equal(9.8, xr2.GetMin())
|
||||
assert.Equal(19.8, xr2.GetMax())
|
||||
testutil.AssertEqual(t, 9.8, xr2.GetMin())
|
||||
testutil.AssertEqual(t, 19.8, xr2.GetMax())
|
||||
|
||||
assert.Equal(9.9, yr2.GetMin())
|
||||
assert.Equal(19.9, yr2.GetMax())
|
||||
testutil.AssertEqual(t, 9.9, yr2.GetMin())
|
||||
testutil.AssertEqual(t, 19.9, yr2.GetMax())
|
||||
|
||||
assert.Equal(9.7, yra2.GetMin())
|
||||
assert.Equal(19.7, yra2.GetMax())
|
||||
testutil.AssertEqual(t, 9.7, yra2.GetMin())
|
||||
testutil.AssertEqual(t, 19.7, yra2.GetMax())
|
||||
}
|
||||
|
||||
func TestChartGetRangesUseTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
// this test asserts that ticks should supercede manual ranges when generating the overall ranges.
|
||||
|
||||
|
@ -155,15 +155,15 @@ func TestChartGetRangesUseTicks(t *testing.T) {
|
|||
}
|
||||
|
||||
xr, yr, yar := c.getRanges()
|
||||
assert.Equal(-2.0, xr.GetMin())
|
||||
assert.Equal(2.0, xr.GetMax())
|
||||
assert.Equal(0.0, yr.GetMin())
|
||||
assert.Equal(5.0, yr.GetMax())
|
||||
assert.True(yar.IsZero(), yar.String())
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
YAxis: YAxis{
|
||||
|
@ -181,15 +181,15 @@ func TestChartGetRangesUseUserRanges(t *testing.T) {
|
|||
}
|
||||
|
||||
xr, yr, yar := c.getRanges()
|
||||
assert.Equal(-2.0, xr.GetMin())
|
||||
assert.Equal(2.0, xr.GetMax())
|
||||
assert.Equal(-5.0, yr.GetMin())
|
||||
assert.Equal(5.0, yr.GetMax())
|
||||
assert.True(yar.IsZero(), yar.String())
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Background: Style{
|
||||
|
@ -198,11 +198,11 @@ func TestChartGetBackgroundStyle(t *testing.T) {
|
|||
}
|
||||
|
||||
bs := c.getBackgroundStyle()
|
||||
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
}
|
||||
|
||||
func TestChartGetCanvasStyle(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Canvas: Style{
|
||||
|
@ -211,19 +211,19 @@ func TestChartGetCanvasStyle(t *testing.T) {
|
|||
}
|
||||
|
||||
bs := c.getCanvasStyle()
|
||||
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
}
|
||||
|
||||
func TestChartGetDefaultCanvasBox(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{}
|
||||
canvasBoxDefault := c.getDefaultCanvasBox()
|
||||
assert.False(canvasBoxDefault.IsZero())
|
||||
assert.Equal(DefaultBackgroundPadding.Top, canvasBoxDefault.Top)
|
||||
assert.Equal(DefaultBackgroundPadding.Left, canvasBoxDefault.Left)
|
||||
assert.Equal(c.GetWidth()-DefaultBackgroundPadding.Right, canvasBoxDefault.Right)
|
||||
assert.Equal(c.GetHeight()-DefaultBackgroundPadding.Bottom, canvasBoxDefault.Bottom)
|
||||
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{
|
||||
|
@ -236,15 +236,15 @@ func TestChartGetDefaultCanvasBox(t *testing.T) {
|
|||
},
|
||||
}
|
||||
canvasBoxCustom := custom.getDefaultCanvasBox()
|
||||
assert.False(canvasBoxCustom.IsZero())
|
||||
assert.Equal(DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top)
|
||||
assert.Equal(DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left)
|
||||
assert.Equal(c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right)
|
||||
assert.Equal(c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom)
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
|
@ -265,90 +265,95 @@ func TestChartGetValueFormatters(t *testing.T) {
|
|||
}
|
||||
|
||||
dxf, dyf, dyaf := c.getValueFormatters()
|
||||
assert.NotNil(dxf)
|
||||
assert.NotNil(dyf)
|
||||
assert.NotNil(dyaf)
|
||||
testutil.AssertNotNil(t, dxf)
|
||||
testutil.AssertNotNil(t, dyf)
|
||||
testutil.AssertNotNil(t, dyaf)
|
||||
}
|
||||
|
||||
func TestChartHasAxes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
assert.False(Chart{}.hasAxes())
|
||||
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: Style{
|
||||
Show: true,
|
||||
},
|
||||
Style: Hidden(),
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Style: Shown(),
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Hidden(),
|
||||
},
|
||||
}
|
||||
assert.True(x.hasAxes())
|
||||
testutil.AssertTrue(t, x.hasAxes())
|
||||
|
||||
y := Chart{
|
||||
XAxis: XAxis{
|
||||
Style: Shown(),
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
Style: Hidden(),
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Hidden(),
|
||||
},
|
||||
}
|
||||
assert.True(y.hasAxes())
|
||||
testutil.AssertTrue(t, y.hasAxes())
|
||||
|
||||
ya := Chart{
|
||||
XAxis: XAxis{
|
||||
Style: Hidden(),
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Style: Hidden(),
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
Style: Shown(),
|
||||
},
|
||||
}
|
||||
assert.True(ya.hasAxes())
|
||||
testutil.AssertTrue(t, ya.hasAxes())
|
||||
}
|
||||
|
||||
func TestChartGetAxesTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
r, err := PNG(1024, 1024)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
c := Chart{
|
||||
XAxis: XAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||
},
|
||||
}
|
||||
xr, yr, yar := c.getRanges()
|
||||
|
||||
xt, yt, yat := c.getAxesTicks(r, xr, yr, yar, FloatValueFormatter, FloatValueFormatter, FloatValueFormatter)
|
||||
assert.NotEmpty(xt)
|
||||
assert.NotEmpty(yt)
|
||||
assert.NotEmpty(yat)
|
||||
testutil.AssertNotEmpty(t, xt)
|
||||
testutil.AssertNotEmpty(t, yt)
|
||||
testutil.AssertNotEmpty(t, yat)
|
||||
}
|
||||
|
||||
func TestChartSingleSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
now := time.Now()
|
||||
c := Chart{
|
||||
Title: "Hello!",
|
||||
TitleStyle: StyleShow(),
|
||||
Width: 1024,
|
||||
Height: 400,
|
||||
Title: "Hello!",
|
||||
Width: 1024,
|
||||
Height: 400,
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
Range: &ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 4.0,
|
||||
},
|
||||
},
|
||||
XAxis: XAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Series: []Series{
|
||||
TimeSeries{
|
||||
Name: "goog",
|
||||
|
@ -360,11 +365,11 @@ func TestChartSingleSeries(t *testing.T) {
|
|||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.NotEmpty(buffer.Bytes())
|
||||
testutil.AssertNotEmpty(t, buffer.Bytes())
|
||||
}
|
||||
|
||||
func TestChartRegressionBadRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
|
@ -376,11 +381,11 @@ func TestChartRegressionBadRanges(t *testing.T) {
|
|||
}
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.True(true, "Render needs to finish.")
|
||||
testutil.AssertTrue(t, true, "Render needs to finish.")
|
||||
}
|
||||
|
||||
func TestChartRegressionBadRangesByUser(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
YAxis: YAxis{
|
||||
|
@ -391,43 +396,43 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
|
|||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.True(true, "Render needs to finish.")
|
||||
testutil.AssertTrue(t, true, "Render needs to finish.")
|
||||
}
|
||||
|
||||
func TestChartValidatesSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(c.validateSeries())
|
||||
testutil.AssertNil(t, c.validateSeries())
|
||||
|
||||
c = Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotNil(c.validateSeries())
|
||||
testutil.AssertNotNil(t, c.validateSeries())
|
||||
}
|
||||
|
||||
func TestChartCheckRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
|
@ -439,27 +444,11 @@ func TestChartCheckRanges(t *testing.T) {
|
|||
}
|
||||
|
||||
xr, yr, yra := c.getRanges()
|
||||
assert.Nil(c.checkRanges(xr, yr, yra))
|
||||
}
|
||||
|
||||
func TestChartCheckRangesFailure(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0},
|
||||
YValues: []float64{3.14, 3.14},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xr, yr, yra := c.getRanges()
|
||||
assert.NotNil(c.checkRanges(xr, yr, yra))
|
||||
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
|
||||
}
|
||||
|
||||
func TestChartCheckRangesWithRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
XAxis: XAxis{
|
||||
|
@ -483,7 +472,7 @@ func TestChartCheckRangesWithRanges(t *testing.T) {
|
|||
}
|
||||
|
||||
xr, yr, yra := c.getRanges()
|
||||
assert.Nil(c.checkRanges(xr, yr, yra))
|
||||
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
|
||||
}
|
||||
|
||||
func at(i image.Image, x, y int) drawing.Color {
|
||||
|
@ -491,85 +480,115 @@ func at(i image.Image, x, y int) drawing.Color {
|
|||
}
|
||||
|
||||
func TestChartE2ELine(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
c := Chart{
|
||||
Height: 50,
|
||||
Width: 50,
|
||||
Height: 50,
|
||||
Width: 50,
|
||||
TitleStyle: Hidden(),
|
||||
XAxis: HideXAxis(),
|
||||
YAxis: HideYAxis(),
|
||||
YAxisSecondary: HideYAxis(),
|
||||
Canvas: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
Padding: BoxZero,
|
||||
},
|
||||
Background: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
Padding: BoxZero,
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(0, 4, 1),
|
||||
YValues: Sequence.Float64(0, 4, 1),
|
||||
XValues: LinearRangeWithStep(0, 4, 1),
|
||||
YValues: LinearRangeWithStep(0, 4, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buffer = &bytes.Buffer{}
|
||||
err := c.Render(PNG, buffer)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
// do color tests ...
|
||||
|
||||
i, err := png.Decode(buffer)
|
||||
assert.Nil(err)
|
||||
testutil.AssertNil(t, err)
|
||||
|
||||
// test the bottom and top of the line
|
||||
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
|
||||
assert.Equal(drawing.ColorWhite, at(i, 49, 49))
|
||||
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)
|
||||
assert.Equal(defaultSeriesColor, at(i, 0, 49))
|
||||
assert.Equal(defaultSeriesColor, at(i, 49, 0))
|
||||
assert.Equal(drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
logBuffer := new(bytes.Buffer)
|
||||
|
||||
c := Chart{
|
||||
Height: 50,
|
||||
Width: 50,
|
||||
Canvas: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
Padding: BoxZero,
|
||||
},
|
||||
Background: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
Padding: BoxZero,
|
||||
},
|
||||
TitleStyle: Hidden(),
|
||||
XAxis: HideXAxis(),
|
||||
YAxis: HideYAxis(),
|
||||
YAxisSecondary: HideYAxis(),
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
StrokeColor: drawing.ColorBlue,
|
||||
FillColor: drawing.ColorRed,
|
||||
},
|
||||
XValues: Sequence.Float64(0, 4, 1),
|
||||
YValues: Sequence.Float64(0, 4, 1),
|
||||
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 buffer = &bytes.Buffer{}
|
||||
err := c.Render(PNG, buffer)
|
||||
assert.Nil(err)
|
||||
|
||||
// do color tests ...
|
||||
|
||||
i, err := png.Decode(buffer)
|
||||
assert.Nil(err)
|
||||
|
||||
// test the bottom and top of the line
|
||||
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
|
||||
assert.Equal(drawing.ColorRed, at(i, 49, 49))
|
||||
|
||||
// test a line mid point
|
||||
defaultSeriesColor := drawing.ColorBlue
|
||||
assert.Equal(defaultSeriesColor, at(i, 0, 49))
|
||||
assert.Equal(defaultSeriesColor, at(i, 49, 0))
|
||||
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
outputPath = flag.String("output", "", "The output file")
|
||||
|
||||
inputFormat = flag.String("format", "csv", "The input format, either 'csv' or 'tsv' (defaults to 'csv')")
|
||||
inputPath = flag.String("f", "", "The input file")
|
||||
reverse = flag.Bool("reverse", false, "If we should reverse the inputs")
|
||||
|
||||
hideLegend = flag.Bool("hide-legend", false, "If we should omit the chart legend")
|
||||
hideSMA = flag.Bool("hide-sma", false, "If we should omit simple moving average")
|
||||
hideLinreg = flag.Bool("hide-linreg", false, "If we should omit linear regressions")
|
||||
hideLastValues = flag.Bool("hide-last-values", false, "If we should omit last values")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log := chart.NewLogger()
|
||||
|
||||
var rawData []byte
|
||||
var err error
|
||||
if *inputPath != "" {
|
||||
if *inputPath == "-" {
|
||||
rawData, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
} else {
|
||||
rawData, err = ioutil.ReadFile(*inputPath)
|
||||
if err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
}
|
||||
} else if len(flag.Args()) > 0 {
|
||||
rawData = []byte(flag.Args()[0])
|
||||
} else {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var parts []string
|
||||
switch *inputFormat {
|
||||
case "csv":
|
||||
parts = chart.SplitCSV(string(rawData))
|
||||
case "tsv":
|
||||
parts = strings.Split(string(rawData), "\t")
|
||||
default:
|
||||
log.FatalErr(fmt.Errorf("invalid format; must be 'csv' or 'tsv'"))
|
||||
}
|
||||
|
||||
yvalues, err := chart.ParseFloats(parts...)
|
||||
if err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
|
||||
if *reverse {
|
||||
yvalues = chart.ValueSequence(yvalues...).Reverse().Values()
|
||||
}
|
||||
|
||||
var series []chart.Series
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "Values",
|
||||
XValues: chart.LinearRange(1, float64(len(yvalues))),
|
||||
YValues: yvalues,
|
||||
}
|
||||
series = append(series, mainSeries)
|
||||
|
||||
smaSeries := &chart.SMASeries{
|
||||
Name: "SMA",
|
||||
Style: chart.Style{
|
||||
Hidden: *hideSMA,
|
||||
StrokeColor: chart.ColorRed,
|
||||
StrokeDashArray: []float64{5.0, 5.0},
|
||||
},
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
series = append(series, smaSeries)
|
||||
|
||||
linRegSeries := &chart.LinearRegressionSeries{
|
||||
Name: "Values - Lin. Reg.",
|
||||
Style: chart.Style{
|
||||
Hidden: *hideLinreg,
|
||||
},
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
series = append(series, linRegSeries)
|
||||
|
||||
mainLastValue := chart.LastValueAnnotationSeries(mainSeries)
|
||||
mainLastValue.Style = chart.Style{
|
||||
Hidden: *hideLastValues,
|
||||
}
|
||||
series = append(series, mainLastValue)
|
||||
|
||||
linregLastValue := chart.LastValueAnnotationSeries(linRegSeries)
|
||||
linregLastValue.Style = chart.Style{
|
||||
Hidden: (*hideLastValues || *hideLinreg),
|
||||
}
|
||||
series = append(series, linregLastValue)
|
||||
|
||||
smaLastValue := chart.LastValueAnnotationSeries(smaSeries)
|
||||
smaLastValue.Style = chart.Style{
|
||||
Hidden: (*hideLastValues || *hideSMA),
|
||||
}
|
||||
series = append(series, smaLastValue)
|
||||
|
||||
graph := chart.Chart{
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 50,
|
||||
},
|
||||
},
|
||||
Series: series,
|
||||
}
|
||||
|
||||
if !*hideLegend {
|
||||
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
|
||||
}
|
||||
|
||||
var output *os.File
|
||||
if *outputPath != "" {
|
||||
output, err = os.Create(*outputPath)
|
||||
if err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
} else {
|
||||
output, err = ioutil.TempFile("", "*.png")
|
||||
if err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := graph.Render(chart.PNG, output); err != nil {
|
||||
log.FatalErr(err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stdout, output.Name())
|
||||
os.Exit(0)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package chart
|
||||
|
||||
import "github.com/wcharczuk/go-chart/drawing"
|
||||
import "git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
|
||||
var (
|
||||
// ColorWhite is white.
|
||||
|
|
|
@ -7,7 +7,7 @@ type ConcatSeries []Series
|
|||
func (cs ConcatSeries) Len() int {
|
||||
total := 0
|
||||
for _, s := range cs {
|
||||
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
total += typed.Len()
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,10 @@ func (cs ConcatSeries) Len() int {
|
|||
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
|
||||
cursor := 0
|
||||
for _, s := range cs {
|
||||
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
len := typed.Len()
|
||||
if index < cursor+len {
|
||||
x, y = typed.GetValue(index - cursor) //FENCEPOSTS.
|
||||
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
|
||||
return
|
||||
}
|
||||
cursor += typed.Len()
|
||||
|
|
|
@ -3,39 +3,39 @@ package chart
|
|||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestConcatSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
s1 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
|
||||
s2 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(11, 20.0),
|
||||
YValues: Sequence.Float64(10.0, 1.0),
|
||||
XValues: LinearRange(11, 20.0),
|
||||
YValues: LinearRange(10.0, 1.0),
|
||||
}
|
||||
|
||||
s3 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(21, 30.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(21, 30.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
|
||||
cs := ConcatSeries([]Series{s1, s2, s3})
|
||||
assert.Equal(30, cs.Len())
|
||||
testutil.AssertEqual(t, 30, cs.Len())
|
||||
|
||||
x0, y0 := cs.GetValue(0)
|
||||
assert.Equal(1.0, x0)
|
||||
assert.Equal(1.0, y0)
|
||||
testutil.AssertEqual(t, 1.0, x0)
|
||||
testutil.AssertEqual(t, 1.0, y0)
|
||||
|
||||
xm, ym := cs.GetValue(19)
|
||||
assert.Equal(20.0, xm)
|
||||
assert.Equal(1.0, ym)
|
||||
testutil.AssertEqual(t, 20.0, xm)
|
||||
testutil.AssertEqual(t, 1.0, ym)
|
||||
|
||||
xn, yn := cs.GetValue(29)
|
||||
assert.Equal(30.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
testutil.AssertEqual(t, 30.0, xn)
|
||||
testutil.AssertEqual(t, 10.0, yn)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,9 @@ func (r *ContinuousRange) SetDomain(domain int) {
|
|||
|
||||
// String returns a simple string for the ContinuousRange.
|
||||
func (r ContinuousRange) String() string {
|
||||
if r.GetDelta() == 0 {
|
||||
return "ContinuousRange [empty]"
|
||||
}
|
||||
return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,20 +3,20 @@ package chart
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestRangeTranslate(t *testing.T) {
|
||||
assert := assert.New(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 = Math.MinAndMax(values...)
|
||||
r.Min, r.Max = MinMax(values...)
|
||||
|
||||
// delta = ~7.0
|
||||
// value = ~5.0
|
||||
// domain = ~1000
|
||||
// 5/8 * 1000 ~=
|
||||
assert.Equal(0, r.Translate(1.0))
|
||||
assert.Equal(1000, r.Translate(8.0))
|
||||
assert.Equal(572, r.Translate(5.0))
|
||||
testutil.AssertEqual(t, 0, r.Translate(1.0))
|
||||
testutil.AssertEqual(t, 1000, r.Translate(8.0))
|
||||
testutil.AssertEqual(t, 572, r.Translate(5.0))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,13 @@ 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
|
||||
|
@ -31,13 +38,18 @@ func (cs ContinuousSeries) Len() int {
|
|||
return len(cs.XValues)
|
||||
}
|
||||
|
||||
// GetValue gets a value at a given index.
|
||||
func (cs ContinuousSeries) GetValue(index int) (float64, float64) {
|
||||
// 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]
|
||||
}
|
||||
|
||||
// GetLastValue gets the last value.
|
||||
func (cs ContinuousSeries) GetLastValue() (float64, float64) {
|
||||
// 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]
|
||||
}
|
||||
|
||||
|
@ -70,11 +82,15 @@ func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang
|
|||
// Validate validates the series.
|
||||
func (cs ContinuousSeries) Validate() error {
|
||||
if len(cs.XValues) == 0 {
|
||||
return fmt.Errorf("continuous series must have xvalues set")
|
||||
return fmt.Errorf("continuous series; must have xvalues set")
|
||||
}
|
||||
|
||||
if len(cs.YValues) == 0 {
|
||||
return fmt.Errorf("continuous series must have yvalues set")
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,35 +4,35 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestContinuousSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
|
||||
assert.Equal("Test Series", cs.GetName())
|
||||
assert.Equal(10, cs.Len())
|
||||
x0, y0 := cs.GetValue(0)
|
||||
assert.Equal(1.0, x0)
|
||||
assert.Equal(1.0, y0)
|
||||
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.GetValue(9)
|
||||
assert.Equal(10.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
xn, yn := cs.GetValues(9)
|
||||
testutil.AssertEqual(t, 10.0, xn)
|
||||
testutil.AssertEqual(t, 10.0, yn)
|
||||
|
||||
xn, yn = cs.GetLastValue()
|
||||
assert.Equal(10.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
xn, yn = cs.GetLastValues()
|
||||
testutil.AssertEqual(t, 10.0, xn)
|
||||
testutil.AssertEqual(t, 10.0, yn)
|
||||
}
|
||||
|
||||
func TestContinuousSeriesValueFormatter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
cs := ContinuousSeries{
|
||||
XValueFormatter: func(v interface{}) string {
|
||||
|
@ -44,29 +44,29 @@ func TestContinuousSeriesValueFormatter(t *testing.T) {
|
|||
}
|
||||
|
||||
xf, yf := cs.GetValueFormatters()
|
||||
assert.Equal("0.100000 foo", xf(0.1))
|
||||
assert.Equal("0.100000 bar", yf(0.1))
|
||||
testutil.AssertEqual(t, "0.100000 foo", xf(0.1))
|
||||
testutil.AssertEqual(t, "0.100000 bar", yf(0.1))
|
||||
}
|
||||
|
||||
func TestContinuousSeriesValidate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
assert.Nil(cs.Validate())
|
||||
testutil.AssertNil(t, cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
XValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
testutil.AssertNotNil(t, cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: LinearRange(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
testutil.AssertNotNil(t, cs.Validate())
|
||||
}
|
||||
|
|
426
date.go
|
@ -1,426 +0,0 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllDaysMask is a bitmask of all the days of the week.
|
||||
AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
|
||||
// WeekDaysMask is a bitmask of all the weekdays of the week.
|
||||
WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
|
||||
//WeekendDaysMask is a bitmask of the weekend days of the week.
|
||||
WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
|
||||
)
|
||||
|
||||
var (
|
||||
// DaysOfWeek are all the time.Weekday in an array for utility purposes.
|
||||
DaysOfWeek = []time.Weekday{
|
||||
time.Sunday,
|
||||
time.Monday,
|
||||
time.Tuesday,
|
||||
time.Wednesday,
|
||||
time.Thursday,
|
||||
time.Friday,
|
||||
time.Saturday,
|
||||
}
|
||||
|
||||
// WeekDays are the business time.Weekday in an array.
|
||||
WeekDays = []time.Weekday{
|
||||
time.Monday,
|
||||
time.Tuesday,
|
||||
time.Wednesday,
|
||||
time.Thursday,
|
||||
time.Friday,
|
||||
}
|
||||
|
||||
// WeekendDays are the weekend time.Weekday in an array.
|
||||
WeekendDays = []time.Weekday{
|
||||
time.Sunday,
|
||||
time.Saturday,
|
||||
}
|
||||
|
||||
//Epoch is unix epoc saved for utility purposes.
|
||||
Epoch = time.Unix(0, 0)
|
||||
)
|
||||
|
||||
var (
|
||||
_easternLock sync.Mutex
|
||||
_eastern *time.Location
|
||||
)
|
||||
|
||||
// NYSEOpen is when the NYSE opens.
|
||||
func NYSEOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEClose is when the NYSE closes.
|
||||
func NYSEClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NASDAQOpen is when NASDAQ opens.
|
||||
func NASDAQOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NASDAQClose is when NASDAQ closes.
|
||||
func NASDAQClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEArcaOpen is when NYSEARCA opens.
|
||||
func NYSEArcaOpen() time.Time { return Date.Time(4, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEArcaClose is when NYSEARCA closes.
|
||||
func NYSEArcaClose() time.Time { return Date.Time(20, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// HolidayProvider is a function that returns if a given time falls on a holiday.
|
||||
type HolidayProvider func(time.Time) bool
|
||||
|
||||
// defaultHolidayProvider implements `HolidayProvider` and just returns false.
|
||||
func defaultHolidayProvider(_ time.Time) bool { return false }
|
||||
|
||||
var (
|
||||
// Date contains utility functions that operate on dates.
|
||||
Date = &date{}
|
||||
)
|
||||
|
||||
type date struct{}
|
||||
|
||||
// IsNYSEHoliday returns if a date was/is on a nyse holiday day.
|
||||
func (d date) IsNYSEHoliday(t time.Time) bool {
|
||||
te := t.In(d.Eastern())
|
||||
if te.Year() == 2013 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 21
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 18
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 29
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 27
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 2
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 28
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2014 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 20
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 17
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 18
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 26
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 1
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 27
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2015 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 19
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 16
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 25
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 7
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 26
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2016 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 18
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 15
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 25
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 30
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 5
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 24 || te.Day() == 25
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 26
|
||||
}
|
||||
} else if te.Year() == 2017 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 16
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 20
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 15
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 29
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 23
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2018 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 15
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 19
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 30
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 28
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 22
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNYSEArcaHoliday returns that returns if a given time falls on a holiday.
|
||||
func (d date) IsNYSEArcaHoliday(t time.Time) bool {
|
||||
return d.IsNYSEHoliday(t)
|
||||
}
|
||||
|
||||
// IsNASDAQHoliday returns if a date was a NASDAQ holiday day.
|
||||
func (d date) IsNASDAQHoliday(t time.Time) bool {
|
||||
return d.IsNYSEHoliday(t)
|
||||
}
|
||||
|
||||
// Time returns a new time.Time for the given clock components.
|
||||
func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time {
|
||||
return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
|
||||
}
|
||||
|
||||
func (d date) Date(year, month, day int, loc *time.Location) time.Time {
|
||||
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
// On returns the clock components of clock (hour,minute,second) on the date components of d.
|
||||
func (d date) On(clock, cd time.Time) time.Time {
|
||||
tzAdjusted := cd.In(clock.Location())
|
||||
return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
|
||||
}
|
||||
|
||||
// NoonOn is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date.
|
||||
func (d date) NoonOn(cd time.Time) time.Time {
|
||||
return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location())
|
||||
}
|
||||
|
||||
// Optional returns a pointer reference to a given time.
|
||||
func (d date) Optional(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// IsWeekDay returns if the day is a monday->friday.
|
||||
func (d date) IsWeekDay(day time.Weekday) bool {
|
||||
return !d.IsWeekendDay(day)
|
||||
}
|
||||
|
||||
// IsWeekendDay returns if the day is a monday->friday.
|
||||
func (d date) IsWeekendDay(day time.Weekday) bool {
|
||||
return day == time.Saturday || day == time.Sunday
|
||||
}
|
||||
|
||||
// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
|
||||
func (d date) Before(before, reference time.Time) bool {
|
||||
tzAdjustedBefore := before.In(reference.Location())
|
||||
if tzAdjustedBefore.Year() < reference.Year() {
|
||||
return true
|
||||
}
|
||||
if tzAdjustedBefore.Month() < reference.Month() {
|
||||
return true
|
||||
}
|
||||
return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day()
|
||||
}
|
||||
|
||||
// NextMarketOpen returns the next market open after a given time.
|
||||
func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||
afterLocalized := after.In(openTime.Location())
|
||||
todaysOpen := d.On(openTime, afterLocalized)
|
||||
|
||||
if isHoliday == nil {
|
||||
isHoliday = defaultHolidayProvider
|
||||
}
|
||||
|
||||
todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen)
|
||||
|
||||
if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay {
|
||||
return todaysOpen
|
||||
}
|
||||
|
||||
for cursorDay := 1; cursorDay < 7; cursorDay++ {
|
||||
newDay := todaysOpen.AddDate(0, 0, cursorDay)
|
||||
isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay)
|
||||
if isValidTradingDay {
|
||||
return d.On(openTime, newDay)
|
||||
}
|
||||
}
|
||||
panic("Have exhausted day window looking for next market open.")
|
||||
}
|
||||
|
||||
// NextMarketClose returns the next market close after a given time.
|
||||
func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||
afterLocalized := after.In(closeTime.Location())
|
||||
|
||||
if isHoliday == nil {
|
||||
isHoliday = defaultHolidayProvider
|
||||
}
|
||||
|
||||
todaysClose := d.On(closeTime, afterLocalized)
|
||||
if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
|
||||
return todaysClose
|
||||
}
|
||||
|
||||
if afterLocalized.Equal(todaysClose) { //rare but it might happen.
|
||||
return todaysClose
|
||||
}
|
||||
|
||||
for cursorDay := 1; cursorDay < 6; cursorDay++ {
|
||||
newDay := todaysClose.AddDate(0, 0, cursorDay)
|
||||
if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) {
|
||||
return d.On(closeTime, newDay)
|
||||
}
|
||||
}
|
||||
panic("Have exhausted day window looking for next market close.")
|
||||
}
|
||||
|
||||
// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates.
|
||||
func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) {
|
||||
startEastern := start.In(d.Eastern())
|
||||
endEastern := end.In(d.Eastern())
|
||||
|
||||
startMarketOpen := d.On(marketOpen, startEastern)
|
||||
startMarketClose := d.On(marketClose, startEastern)
|
||||
|
||||
if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) {
|
||||
if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) {
|
||||
if endEastern.Before(startMarketClose) {
|
||||
seconds += int64(endEastern.Sub(startEastern) / time.Second)
|
||||
} else {
|
||||
seconds += int64(startMarketClose.Sub(startEastern) / time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday)
|
||||
for d.Before(cursor, endEastern) {
|
||||
if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) {
|
||||
close := d.NextMarketClose(cursor, marketClose, isHoliday)
|
||||
seconds += int64(close.Sub(cursor) / time.Second)
|
||||
}
|
||||
cursor = cursor.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday)
|
||||
finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday)
|
||||
if endEastern.After(finalMarketOpen) {
|
||||
if endEastern.Before(finalMarketClose) {
|
||||
seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second)
|
||||
} else {
|
||||
seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
_secondsPerHour = 60 * 60
|
||||
_secondsPerDay = 60 * 60 * 24
|
||||
)
|
||||
|
||||
func (d date) DiffDays(t1, t2 time.Time) (days int) {
|
||||
t1n := t1.Unix()
|
||||
t2n := t2.Unix()
|
||||
diff := t2n - t1n //yields seconds
|
||||
return int(diff / (_secondsPerDay))
|
||||
}
|
||||
|
||||
func (d date) DiffHours(t1, t2 time.Time) (hours int) {
|
||||
t1n := t1.Unix()
|
||||
t2n := t2.Unix()
|
||||
diff := t2n - t1n //yields seconds
|
||||
return int(diff / (_secondsPerHour))
|
||||
}
|
||||
|
||||
// NextDay returns the timestamp advanced a day.
|
||||
func (d date) NextDay(ts time.Time) time.Time {
|
||||
return ts.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
// NextHour returns the next timestamp on the hour.
|
||||
func (d date) NextHour(ts time.Time) time.Time {
|
||||
//advance a full hour ...
|
||||
advanced := ts.Add(time.Hour)
|
||||
minutes := time.Duration(advanced.Minute()) * time.Minute
|
||||
final := advanced.Add(-minutes)
|
||||
return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location())
|
||||
}
|
||||
|
||||
// NextDayOfWeek returns the next instance of a given weekday after a given timestamp.
|
||||
func (d date) NextDayOfWeek(after time.Time, dayOfWeek time.Weekday) time.Time {
|
||||
afterWeekday := after.Weekday()
|
||||
if afterWeekday == dayOfWeek {
|
||||
return after.AddDate(0, 0, 7)
|
||||
}
|
||||
|
||||
// 1 vs 5 ~ add 4 days
|
||||
if afterWeekday < dayOfWeek {
|
||||
dayDelta := int(dayOfWeek - afterWeekday)
|
||||
return after.AddDate(0, 0, dayDelta)
|
||||
}
|
||||
|
||||
// 5 vs 1, add 7-(5-1) ~ 3 days
|
||||
dayDelta := 7 - int(afterWeekday-dayOfWeek)
|
||||
return after.AddDate(0, 0, dayDelta)
|
||||
}
|
||||
|
||||
// Start returns the earliest (min) time in a list of times.
|
||||
func (d date) Start(times []time.Time) time.Time {
|
||||
if len(times) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
start := times[0]
|
||||
for _, t := range times[1:] {
|
||||
if t.Before(start) {
|
||||
start = t
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// Start returns the earliest (min) time in a list of times.
|
||||
func (d date) End(times []time.Time) time.Time {
|
||||
if len(times) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
end := times[0]
|
||||
for _, t := range times[1:] {
|
||||
if t.After(end) {
|
||||
end = t
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package chart
|
||||
|
||||
import "time"
|
||||
|
||||
// Eastern returns the eastern timezone.
|
||||
func (d date) Eastern() *time.Location {
|
||||
if _eastern == nil {
|
||||
_easternLock.Lock()
|
||||
defer _easternLock.Unlock()
|
||||
if _eastern == nil {
|
||||
_eastern, _ = time.LoadLocation("America/New_York")
|
||||
}
|
||||
}
|
||||
return _eastern
|
||||
}
|
288
date_test.go
|
@ -1,288 +0,0 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func parse(v string) time.Time {
|
||||
ts, _ := time.Parse("2006-01-02", v)
|
||||
return ts
|
||||
}
|
||||
|
||||
func TestDateTime(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.Time(5, 6, 7, 8, time.UTC)
|
||||
assert.Equal(05, ts.Hour())
|
||||
assert.Equal(06, ts.Minute())
|
||||
assert.Equal(07, ts.Second())
|
||||
assert.Equal(8, ts.Nanosecond())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateDate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.Date(2015, 5, 6, time.UTC)
|
||||
assert.Equal(2015, ts.Year())
|
||||
assert.Equal(5, ts.Month())
|
||||
assert.Equal(6, ts.Day())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateOn(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.On(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.Eastern()))
|
||||
assert.Equal(2016, ts.Year())
|
||||
assert.Equal(6, ts.Month())
|
||||
assert.Equal(7, ts.Day())
|
||||
assert.Equal(5, ts.Hour())
|
||||
assert.Equal(4, ts.Minute())
|
||||
assert.Equal(3, ts.Second())
|
||||
assert.Equal(2, ts.Nanosecond())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateNoonOn(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
noon := Date.NoonOn(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC))
|
||||
|
||||
assert.Equal(2016, noon.Year())
|
||||
assert.Equal(4, noon.Month())
|
||||
assert.Equal(3, noon.Day())
|
||||
assert.Equal(12, noon.Hour())
|
||||
assert.Equal(0, noon.Minute())
|
||||
assert.Equal(time.UTC, noon.Location())
|
||||
}
|
||||
|
||||
func TestDateBefore(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.True(Date.Before(parse("2015-07-02"), parse("2016-07-01")))
|
||||
assert.True(Date.Before(parse("2016-06-01"), parse("2016-07-01")))
|
||||
assert.True(Date.Before(parse("2016-07-01"), parse("2016-07-02")))
|
||||
|
||||
assert.False(Date.Before(parse("2016-07-01"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2016-07-03"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2016-08-03"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2017-08-03"), parse("2016-07-01")))
|
||||
}
|
||||
|
||||
func TestDateBeforeHandlesTimezones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tuesdayUTC := time.Date(2016, 8, 02, 22, 00, 0, 0, time.UTC)
|
||||
mondayUTC := time.Date(2016, 8, 01, 1, 00, 0, 0, time.UTC)
|
||||
sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(Date.Before(sundayEST, tuesdayUTC))
|
||||
assert.False(Date.Before(sundayEST, mondayUTC))
|
||||
}
|
||||
|
||||
func TestNextMarketOpen(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Date.Eastern())
|
||||
todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Date.Eastern())
|
||||
tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Date.Eastern())
|
||||
mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(todayOpen.Equal(Date.NextMarketOpen(beforeOpen, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(tomorrowOpen.Equal(Date.NextMarketOpen(afterOpen, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
|
||||
assert.Equal(Date.Eastern(), todayOpen.Location())
|
||||
assert.Equal(Date.Eastern(), tomorrowOpen.Location())
|
||||
assert.Equal(Date.Eastern(), mondayOpen.Location())
|
||||
|
||||
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern())
|
||||
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(shouldbe.Equal(Date.NextMarketOpen(testRegression, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
}
|
||||
|
||||
func TestNextMarketClose(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Date.Eastern())
|
||||
todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Date.Eastern())
|
||||
tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Date.Eastern())
|
||||
mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Date.Eastern())
|
||||
|
||||
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(todayClose.Equal(Date.NextMarketClose(beforeClose, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
|
||||
assert.Equal(Date.Eastern(), todayClose.Location())
|
||||
assert.Equal(Date.Eastern(), tomorrowClose.Location())
|
||||
assert.Equal(Date.Eastern(), mondayClose.Location())
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetween(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 5 * 6.5 * 60 * 60
|
||||
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetween1D(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2016, 07, 22, 9, 45, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 22, 15, 45, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 6 * 60 * 60
|
||||
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetweenLTM(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year.
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestDateNextHour(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
next := Date.NextHour(start)
|
||||
assert.Equal(2015, next.Year())
|
||||
assert.Equal(07, next.Month())
|
||||
assert.Equal(01, next.Day())
|
||||
assert.Equal(10, next.Hour())
|
||||
assert.Equal(00, next.Minute())
|
||||
|
||||
next = Date.NextHour(next)
|
||||
assert.Equal(11, next.Hour())
|
||||
|
||||
next = Date.NextHour(next)
|
||||
assert.Equal(12, next.Hour())
|
||||
|
||||
}
|
||||
|
||||
func TestDateNextDayOfWeek(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
weds := Date.Date(2016, 8, 10, time.UTC)
|
||||
fri := Date.Date(2016, 8, 12, time.UTC)
|
||||
sun := Date.Date(2016, 8, 14, time.UTC)
|
||||
mon := Date.Date(2016, 8, 15, time.UTC)
|
||||
weds2 := Date.Date(2016, 8, 17, time.UTC)
|
||||
|
||||
nextFri := Date.NextDayOfWeek(weds, time.Friday)
|
||||
nextSunday := Date.NextDayOfWeek(weds, time.Sunday)
|
||||
nextMonday := Date.NextDayOfWeek(weds, time.Monday)
|
||||
nextWeds := Date.NextDayOfWeek(weds, time.Wednesday)
|
||||
|
||||
assert.Equal(fri.Year(), nextFri.Year())
|
||||
assert.Equal(fri.Month(), nextFri.Month())
|
||||
assert.Equal(fri.Day(), nextFri.Day())
|
||||
|
||||
assert.Equal(sun.Year(), nextSunday.Year())
|
||||
assert.Equal(sun.Month(), nextSunday.Month())
|
||||
assert.Equal(sun.Day(), nextSunday.Day())
|
||||
|
||||
assert.Equal(mon.Year(), nextMonday.Year())
|
||||
assert.Equal(mon.Month(), nextMonday.Month())
|
||||
assert.Equal(mon.Day(), nextMonday.Day())
|
||||
|
||||
assert.Equal(weds2.Year(), nextWeds.Year())
|
||||
assert.Equal(weds2.Month(), nextWeds.Month())
|
||||
assert.Equal(weds2.Day(), nextWeds.Day())
|
||||
|
||||
assert.Equal(time.UTC, nextFri.Location())
|
||||
assert.Equal(time.UTC, nextSunday.Location())
|
||||
assert.Equal(time.UTC, nextMonday.Location())
|
||||
}
|
||||
|
||||
func TestDateIsNYSEHoliday(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cursor := time.Date(2013, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
var holidays int
|
||||
for Date.Before(cursor, end) {
|
||||
if Date.IsNYSEHoliday(cursor) {
|
||||
holidays++
|
||||
}
|
||||
cursor = cursor.AddDate(0, 0, 1)
|
||||
}
|
||||
assert.Equal(holidays, 55)
|
||||
}
|
||||
|
||||
func TestTimeStart(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
times := []time.Time{
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
time.Now().AddDate(0, 0, -2),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -5),
|
||||
}
|
||||
|
||||
assert.InTimeDelta(Date.Start(times), times[4], time.Millisecond)
|
||||
}
|
||||
|
||||
func TestTimeEnd(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
times := []time.Time{
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
time.Now().AddDate(0, 0, -2),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -5),
|
||||
}
|
||||
|
||||
assert.InTimeDelta(Date.End(times), times[2], time.Millisecond)
|
||||
}
|
||||
|
||||
func TestDateDiffDays(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2017, 01, 10, 3, 0, 0, 0, time.UTC)
|
||||
t3 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
|
||||
|
||||
assert.Equal(48, Date.DiffDays(t2, t1))
|
||||
assert.Equal(2, Date.DiffDays(t3, t1)) // technically we should round down.
|
||||
}
|
||||
|
||||
func TestDateDiffHours(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
|
||||
t3 := time.Date(2017, 02, 28, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
assert.Equal(68, Date.DiffHours(t2, t1))
|
||||
assert.Equal(24, Date.DiffHours(t1, t3))
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// +build windows
|
||||
|
||||
package chart
|
||||
|
||||
import "time"
|
||||
|
||||
// Eastern returns the eastern timezone.
|
||||
func (d date) Eastern() *time.Location {
|
||||
if _eastern == nil {
|
||||
_easternLock.Lock()
|
||||
defer _easternLock.Unlock()
|
||||
if _eastern == nil {
|
||||
_eastern, _ = time.LoadLocation("EST")
|
||||
}
|
||||
}
|
||||
return _eastern
|
||||
}
|
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestDonutChart(t *testing.T) {
|
||||
// replaced new assertions helper
|
||||
|
||||
pie := DonutChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 10, Label: "Blue"},
|
||||
{Value: 9, Label: "Green"},
|
||||
{Value: 8, Label: "Gray"},
|
||||
{Value: 7, Label: "Orange"},
|
||||
{Value: 6, Label: "HEANG"},
|
||||
{Value: 5, Label: "??"},
|
||||
{Value: 2, Label: "!!"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
pie.Render(PNG, b)
|
||||
testutil.AssertNotZero(t, b.Len())
|
||||
}
|
||||
|
||||
func TestDonutChartDropsZeroValues(t *testing.T) {
|
||||
// replaced new assertions helper
|
||||
|
||||
pie := DonutChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 5, Label: "Blue"},
|
||||
{Value: 5, Label: "Green"},
|
||||
{Value: 0, Label: "Gray"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
err := pie.Render(PNG, b)
|
||||
testutil.AssertNil(t, err)
|
||||
}
|
||||
|
||||
func TestDonutChartAllZeroValues(t *testing.T) {
|
||||
// replaced new assertions helper
|
||||
|
||||
pie := DonutChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 0, Label: "Blue"},
|
||||
{Value: 0, Label: "Green"},
|
||||
{Value: 0, Label: "Gray"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
err := pie.Render(PNG, b)
|
||||
testutil.AssertNotNil(t, err)
|
||||
}
|
36
draw.go
|
@ -1,6 +1,8 @@
|
|||
package chart
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
var (
|
||||
// Draw contains helpers for drawing common objects.
|
||||
|
@ -10,7 +12,7 @@ var (
|
|||
type draw struct{}
|
||||
|
||||
// LineSeries draws a line series with a renderer.
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -18,7 +20,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
||||
v0x, v0y := vs.GetValue(0)
|
||||
v0x, v0y := vs.GetValues(0)
|
||||
x0 := cl + xrange.Translate(v0x)
|
||||
y0 := cb - yrange.Translate(v0y)
|
||||
|
||||
|
@ -31,13 +33,13 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
vx, vy = vs.GetValues(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
}
|
||||
r.LineTo(x, Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x, MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, y0)
|
||||
r.Fill()
|
||||
}
|
||||
|
@ -47,7 +49,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
vx, vy = vs.GetValues(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
|
@ -60,7 +62,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
|
||||
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||
for i := 0; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
vx, vy = vs.GetValues(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
|
||||
|
@ -82,8 +84,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
}
|
||||
}
|
||||
|
||||
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
|
||||
// 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]
|
||||
|
@ -92,7 +94,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
||||
v0x, v0y1, v0y2 := bbs.GetBoundedValue(0)
|
||||
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
|
||||
x0 := cl + xrange.Translate(v0x)
|
||||
y0 := cb - yrange.Translate(v0y1)
|
||||
|
||||
|
@ -107,7 +109,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < bbs.Len(); i++ {
|
||||
vx, vy1, vy2 = bbs.GetBoundedValue(i)
|
||||
vx, vy1, vy2 = bbs.GetBoundedValues(i)
|
||||
|
||||
xvalues[i] = vx
|
||||
y2values[i] = vy2
|
||||
|
@ -133,7 +135,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
}
|
||||
|
||||
// HistogramSeries draws a value provider as boxes from 0.
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -150,7 +152,7 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
|||
|
||||
//foreach datapoint, draw a box.
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy := vs.GetValue(index)
|
||||
vx, vy := vs.GetValues(index)
|
||||
y0 := yrange.Translate(0)
|
||||
x := cl + xrange.Translate(vx)
|
||||
y := yrange.Translate(vy)
|
||||
|
@ -294,8 +296,10 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
|||
switch style.GetTextVerticalAlign() {
|
||||
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
||||
y = y - linesBox.Height()
|
||||
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
|
||||
y = (y - linesBox.Height()) >> 1
|
||||
case TextVerticalAlignMiddle:
|
||||
y = y + (box.Height() >> 1) - (linesBox.Height() >> 1)
|
||||
case TextVerticalAlignMiddleBaseline:
|
||||
y = y + (box.Height() >> 1) - linesBox.Height()
|
||||
}
|
||||
|
||||
var tx, ty int
|
||||
|
|
162
drawing/color.go
|
@ -2,27 +2,46 @@ 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{}
|
||||
|
||||
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: 255, B: 0, A: 255}
|
||||
|
||||
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 {
|
||||
|
@ -30,8 +49,97 @@ func parseHex(hex string) uint8 {
|
|||
return uint8(v)
|
||||
}
|
||||
|
||||
// ParseColor parses a color from a string.
|
||||
func ParseColor(rawColor string) Color {
|
||||
if strings.HasPrefix(rawColor, "rgba") {
|
||||
return ColorFromRGBA(rawColor)
|
||||
}
|
||||
if strings.HasPrefix(rawColor, "rgb") {
|
||||
return ColorFromRGB(rawColor)
|
||||
}
|
||||
if strings.HasPrefix(rawColor, "#") {
|
||||
return ColorFromHex(rawColor)
|
||||
}
|
||||
return ColorFromKnown(rawColor)
|
||||
}
|
||||
|
||||
var rgbaexpr = regexp.MustCompile(`rgba\((?P<R>.+),(?P<G>.+),(?P<B>.+),(?P<A>.+)\)`)
|
||||
|
||||
// ColorFromRGBA returns a color from an `rgba()` css function.
|
||||
func ColorFromRGBA(rgba string) (output Color) {
|
||||
values := rgbaexpr.FindStringSubmatch(rgba)
|
||||
for i, name := range rgbaexpr.SubexpNames() {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
if i >= len(values) {
|
||||
break
|
||||
}
|
||||
switch name {
|
||||
case "R":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.R = uint8(parsed)
|
||||
case "G":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.G = uint8(parsed)
|
||||
case "B":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.B = uint8(parsed)
|
||||
case "A":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseFloat(value, 32)
|
||||
if parsed > 1 {
|
||||
parsed = 1
|
||||
} else if parsed < 0 {
|
||||
parsed = 0
|
||||
}
|
||||
output.A = uint8(parsed * 255)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var rgbexpr = regexp.MustCompile(`rgb\((?P<R>.+),(?P<G>.+),(?P<B>.+)\)`)
|
||||
|
||||
// ColorFromRGB returns a color from an `rgb()` css function.
|
||||
func ColorFromRGB(rgb string) (output Color) {
|
||||
output.A = 255
|
||||
values := rgbexpr.FindStringSubmatch(rgb)
|
||||
for i, name := range rgbaexpr.SubexpNames() {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
if i >= len(values) {
|
||||
break
|
||||
}
|
||||
switch name {
|
||||
case "R":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.R = uint8(parsed)
|
||||
case "G":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.G = uint8(parsed)
|
||||
case "B":
|
||||
value := strings.TrimSpace(values[i])
|
||||
parsed, _ := strconv.ParseInt(value, 10, 16)
|
||||
output.B = uint8(parsed)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ColorFromHex returns a color from a css hex code.
|
||||
//
|
||||
// NOTE: it will trim a leading '#' character if present.
|
||||
func ColorFromHex(hex string) Color {
|
||||
if strings.HasPrefix(hex, "#") {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
}
|
||||
var c Color
|
||||
if len(hex) == 3 {
|
||||
c.R = parseHex(string(hex[0])) * 0x11
|
||||
|
@ -46,6 +154,46 @@ func ColorFromHex(hex string) Color {
|
|||
return c
|
||||
}
|
||||
|
||||
// ColorFromKnown returns an internal color from a known (basic) color name.
|
||||
func ColorFromKnown(known string) Color {
|
||||
switch strings.ToLower(known) {
|
||||
case "transparent":
|
||||
return ColorTransparent
|
||||
case "white":
|
||||
return ColorWhite
|
||||
case "black":
|
||||
return ColorBlack
|
||||
case "red":
|
||||
return ColorRed
|
||||
case "blue":
|
||||
return ColorBlue
|
||||
case "green":
|
||||
return ColorGreen
|
||||
case "silver":
|
||||
return ColorSilver
|
||||
case "maroon":
|
||||
return ColorMaroon
|
||||
case "purple":
|
||||
return ColorPurple
|
||||
case "fuchsia":
|
||||
return ColorFuchsia
|
||||
case "lime":
|
||||
return ColorLime
|
||||
case "olive":
|
||||
return ColorOlive
|
||||
case "yellow":
|
||||
return ColorYellow
|
||||
case "navy":
|
||||
return ColorNavy
|
||||
case "teal":
|
||||
return ColorTeal
|
||||
case "aqua":
|
||||
return ColorAqua
|
||||
default:
|
||||
return Color{}
|
||||
}
|
||||
}
|
||||
|
||||
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
|
||||
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
|
||||
fa := float64(a) / 255.0
|
||||
|
|
|
@ -1,53 +1,114 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"image/color"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
func TestColorFromHex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
white := ColorFromHex("FFFFFF")
|
||||
assert.Equal(ColorWhite, white)
|
||||
testutil.AssertEqual(t, ColorWhite, white)
|
||||
|
||||
shortWhite := ColorFromHex("FFF")
|
||||
assert.Equal(ColorWhite, shortWhite)
|
||||
testutil.AssertEqual(t, ColorWhite, shortWhite)
|
||||
|
||||
black := ColorFromHex("000000")
|
||||
assert.Equal(ColorBlack, black)
|
||||
testutil.AssertEqual(t, ColorBlack, black)
|
||||
|
||||
shortBlack := ColorFromHex("000")
|
||||
assert.Equal(ColorBlack, shortBlack)
|
||||
testutil.AssertEqual(t, ColorBlack, shortBlack)
|
||||
|
||||
red := ColorFromHex("FF0000")
|
||||
assert.Equal(ColorRed, red)
|
||||
testutil.AssertEqual(t, ColorRed, red)
|
||||
|
||||
shortRed := ColorFromHex("F00")
|
||||
assert.Equal(ColorRed, shortRed)
|
||||
testutil.AssertEqual(t, ColorRed, shortRed)
|
||||
|
||||
green := ColorFromHex("00FF00")
|
||||
assert.Equal(ColorGreen, green)
|
||||
green := ColorFromHex("008000")
|
||||
testutil.AssertEqual(t, ColorGreen, green)
|
||||
|
||||
shortGreen := ColorFromHex("0F0")
|
||||
assert.Equal(ColorGreen, shortGreen)
|
||||
// shortGreen := ColorFromHex("0F0")
|
||||
// testutil.AssertEqual(t, ColorGreen, shortGreen)
|
||||
|
||||
blue := ColorFromHex("0000FF")
|
||||
assert.Equal(ColorBlue, blue)
|
||||
testutil.AssertEqual(t, ColorBlue, blue)
|
||||
|
||||
shortBlue := ColorFromHex("00F")
|
||||
assert.Equal(ColorBlue, shortBlue)
|
||||
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) {
|
||||
assert := assert.New(t)
|
||||
|
||||
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
|
||||
assert.True(black.Equals(ColorBlack), black.String())
|
||||
testutil.AssertTrue(t, black.Equals(ColorBlack), black.String())
|
||||
|
||||
white := ColorFromAlphaMixedRGBA(color.White.RGBA())
|
||||
assert.True(white.Equals(ColorWhite), white.String())
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package drawing
|
|||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
type point struct {
|
||||
|
@ -23,7 +23,7 @@ func (ml mockLine) Len() int {
|
|||
}
|
||||
|
||||
func TestTraceQuad(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
// Quad
|
||||
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||
|
@ -31,5 +31,5 @@ func TestTraceQuad(t *testing.T) {
|
|||
quad := []float64{10, 20, 20, 20, 20, 10}
|
||||
liner := &mockLine{}
|
||||
TraceQuad(liner, quad, 0.5)
|
||||
assert.NotZero(liner.Len())
|
||||
testutil.AssertNotZero(t, liner.Len())
|
||||
}
|
||||
|
|
|
@ -7,6 +7,13 @@ const (
|
|||
DefaultEMAPeriod = 12
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*EMASeries)(nil)
|
||||
_ FirstValuesProvider = (*EMASeries)(nil)
|
||||
_ LastValuesProvider = (*EMASeries)(nil)
|
||||
)
|
||||
|
||||
// EMASeries is a computed series.
|
||||
type EMASeries struct {
|
||||
Name string
|
||||
|
@ -14,7 +21,7 @@ type EMASeries struct {
|
|||
YAxis YAxisType
|
||||
|
||||
Period int
|
||||
InnerSeries ValueProvider
|
||||
InnerSeries ValuesProvider
|
||||
|
||||
cache []float64
|
||||
}
|
||||
|
@ -52,23 +59,36 @@ func (ema EMASeries) GetSigma() float64 {
|
|||
return 2.0 / (float64(ema.GetPeriod()) + 1)
|
||||
}
|
||||
|
||||
// GetValue gets a value at a given index.
|
||||
func (ema *EMASeries) GetValue(index int) (x, y float64) {
|
||||
// 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.GetValue(index)
|
||||
vx, _ := ema.InnerSeries.GetValues(index)
|
||||
x = vx
|
||||
y = ema.cache[index]
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValue computes the last moving average value but walking back window size samples,
|
||||
// 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) GetLastValue() (x, y float64) {
|
||||
func (ema *EMASeries) GetLastValues() (x, y float64) {
|
||||
if ema.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
|
@ -76,7 +96,7 @@ func (ema *EMASeries) GetLastValue() (x, y float64) {
|
|||
ema.ensureCachedValues()
|
||||
}
|
||||
lastIndex := ema.InnerSeries.Len() - 1
|
||||
x, _ = ema.InnerSeries.GetValue(lastIndex)
|
||||
x, _ = ema.InnerSeries.GetValues(lastIndex)
|
||||
y = ema.cache[lastIndex]
|
||||
return
|
||||
}
|
||||
|
@ -86,7 +106,7 @@ func (ema *EMASeries) ensureCachedValues() {
|
|||
ema.cache = make([]float64, seriesLength)
|
||||
sigma := ema.GetSigma()
|
||||
for x := 0; x < seriesLength; x++ {
|
||||
_, y := ema.InnerSeries.GetValue(x)
|
||||
_, y := ema.InnerSeries.GetValues(x)
|
||||
if x == 0 {
|
||||
ema.cache[x] = y
|
||||
continue
|
||||
|
|
|
@ -3,11 +3,11 @@ package chart
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"git.smarteching.com/zeni/go-chart/v2/testutil"
|
||||
)
|
||||
|
||||
var (
|
||||
emaXValues = Sequence.Float64(1.0, 50.0)
|
||||
emaXValues = LinearRange(1.0, 50.0)
|
||||
emaYValues = []float64{
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
|
@ -73,13 +73,13 @@ var (
|
|||
)
|
||||
|
||||
func TestEMASeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// replaced new assertions helper
|
||||
|
||||
mockSeries := mockValueProvider{
|
||||
mockSeries := mockValuesProvider{
|
||||
emaXValues,
|
||||
emaYValues,
|
||||
}
|
||||
assert.Equal(50, mockSeries.Len())
|
||||
testutil.AssertEqual(t, 50, mockSeries.Len())
|
||||
|
||||
ema := &EMASeries{
|
||||
InnerSeries: mockSeries,
|
||||
|
@ -87,19 +87,19 @@ func TestEMASeries(t *testing.T) {
|
|||
}
|
||||
|
||||
sig := ema.GetSigma()
|
||||
assert.Equal(2.0/(26.0+1), sig)
|
||||
testutil.AssertEqual(t, 2.0/(26.0+1), sig)
|
||||
|
||||
var yvalues []float64
|
||||
for x := 0; x < ema.Len(); x++ {
|
||||
_, y := ema.GetValue(x)
|
||||
_, y := ema.GetValues(x)
|
||||
yvalues = append(yvalues, y)
|
||||
}
|
||||
|
||||
for index, yv := range yvalues {
|
||||
assert.InDelta(yv, emaExpected[index], emaDelta)
|
||||
testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta)
|
||||
}
|
||||
|
||||
lvx, lvy := ema.GetLastValue()
|
||||
assert.Equal(50.0, lvx)
|
||||
assert.InDelta(lvy, emaExpected[49], emaDelta)
|
||||
lvx, lvy := ema.GetLastValues()
|
||||
testutil.AssertEqual(t, 50.0, lvx)
|
||||
testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
||||
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).
|
||||
|
@ -37,11 +38,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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 |
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
chart "git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
func main() {
|
||||
|
||||
/*
|
||||
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||
|
@ -14,20 +16,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
*/
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Name: "The XAxis",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "The YAxis",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
},
|
||||
|
@ -37,11 +28,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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 |
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
chart "git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
func main() {
|
||||
|
||||
/*
|
||||
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||
|
@ -15,19 +17,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the x-axis
|
||||
},
|
||||
Name: "The XAxis",
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the y-axis
|
||||
},
|
||||
Name: "The YAxis",
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
},
|
||||
|
@ -37,11 +34,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
graph := chart.BarChart{
|
||||
Title: "Test Bar Chart",
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 40,
|
||||
},
|
||||
},
|
||||
Height: 512,
|
||||
BarWidth: 60,
|
||||
Bars: []chart.Value{
|
||||
{Value: 5.25, Label: "Blue"},
|
||||
{Value: 4.88, Label: "Green"},
|
||||
{Value: 4.74, Label: "Gray"},
|
||||
{Value: 3.22, Label: "Orange"},
|
||||
{Value: 3, Label: "Test"},
|
||||
{Value: 2.27, Label: "??"},
|
||||
{Value: 1, Label: "!!"},
|
||||
},
|
||||
}
|
||||
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
graph.Render(chart.PNG, f)
|
||||
}
|
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
func main() {
|
||||
profitStyle := chart.Style{
|
||||
FillColor: drawing.ColorFromHex("13c158"),
|
||||
StrokeColor: drawing.ColorFromHex("13c158"),
|
||||
StrokeWidth: 0,
|
||||
}
|
||||
|
||||
lossStyle := chart.Style{
|
||||
FillColor: drawing.ColorFromHex("c11313"),
|
||||
StrokeColor: drawing.ColorFromHex("c11313"),
|
||||
StrokeWidth: 0,
|
||||
}
|
||||
|
||||
sbc := chart.BarChart{
|
||||
Title: "Bar Chart Using BaseValue",
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 40,
|
||||
},
|
||||
},
|
||||
Height: 512,
|
||||
BarWidth: 60,
|
||||
YAxis: chart.YAxis{
|
||||
Ticks: []chart.Tick{
|
||||
{Value: -4.0, Label: "-4"},
|
||||
{Value: -2.0, Label: "-2"},
|
||||
{Value: 0, Label: "0"},
|
||||
{Value: 2.0, Label: "2"},
|
||||
{Value: 4.0, Label: "4"},
|
||||
{Value: 6.0, Label: "6"},
|
||||
{Value: 8.0, Label: "8"},
|
||||
{Value: 10.0, Label: "10"},
|
||||
{Value: 12.0, Label: "12"},
|
||||
},
|
||||
},
|
||||
UseBaseValue: true,
|
||||
BaseValue: 0.0,
|
||||
Bars: []chart.Value{
|
||||
{Value: 10.0, Style: profitStyle, Label: "Profit"},
|
||||
{Value: 12.0, Style: profitStyle, Label: "More Profit"},
|
||||
{Value: 8.0, Style: profitStyle, Label: "Still Profit"},
|
||||
{Value: -4.0, Style: lossStyle, Label: "Loss!"},
|
||||
{Value: 3.0, Style: profitStyle, Label: "Phew Ok"},
|
||||
{Value: -2.0, Style: lossStyle, Label: "Oh No!"},
|
||||
},
|
||||
}
|
||||
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
sbc.Render(chart.PNG, f)
|
||||
}
|
BIN
examples/bar_chart_base_value/output.png
Normal file
After Width: | Height: | Size: 18 KiB |
|
@ -1,26 +1,26 @@
|
|||
package main
|
||||
|
||||
//go:generate go run main.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
sbc := chart.BarChart{
|
||||
Height: 512,
|
||||
BarWidth: 60,
|
||||
XAxis: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
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"},
|
||||
|
@ -33,10 +33,11 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := sbc.Render(chart.PNG, res)
|
||||
err := graph.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering chart: %v\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func port() string {
|
23
examples/basic/main.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package main
|
||||
|
||||
//go:generate go run main.go
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
graph.Render(chart.PNG, f)
|
||||
}
|
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func random(min, max float64) float64 {
|
||||
return rand.Float64()*(max-min) + min
|
||||
}
|
||||
|
||||
func main() {
|
||||
numValues := 1024
|
||||
numSeries := 100
|
||||
series := make([]chart.Series, numSeries)
|
||||
|
||||
for i := 0; i < numSeries; i++ {
|
||||
xValues := make([]time.Time, numValues)
|
||||
yValues := make([]float64, numValues)
|
||||
|
||||
for j := 0; j < numValues; j++ {
|
||||
xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1)
|
||||
yValues[j] = random(float64(-500), float64(500))
|
||||
}
|
||||
|
||||
series[i] = chart.TimeSeries{
|
||||
Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i),
|
||||
XValues: xValues,
|
||||
YValues: yValues,
|
||||
}
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Name: "Time",
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "Value",
|
||||
},
|
||||
Series: series,
|
||||
}
|
||||
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
graph.Render(chart.PNG, f)
|
||||
}
|
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
// Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example
|
||||
|
||||
func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) {
|
||||
res.Write([]byte(
|
||||
"<!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))
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
package main
|
||||
|
||||
//go:generate go run main.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
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.
|
||||
|
@ -16,9 +18,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
ValueFormatter: func(v interface{}) string {
|
||||
if vf, isFloat := v.(float64); isFloat {
|
||||
return fmt.Sprintf("%0.6f", vf)
|
||||
|
@ -33,12 +32,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
func main() {
|
||||
graph := chart.Chart{
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 50,
|
||||
Left: 25,
|
||||
Right: 25,
|
||||
Bottom: 10,
|
||||
},
|
||||
FillColor: drawing.ColorFromHex("efefef"),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(),
|
||||
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(100).WithMax(512)}.Values(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
graph.Render(chart.PNG, f)
|
||||
}
|
BIN
examples/custom_padding/output.png
Normal file
After Width: | Height: | Size: 65 KiB |
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
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.
|
||||
|
@ -14,9 +16,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 10.0,
|
||||
|
@ -29,12 +28,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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 |
|
@ -1,13 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
func main() {
|
||||
/*
|
||||
In this example we set some custom colors for the series and the chart background and canvas.
|
||||
*/
|
||||
|
@ -21,7 +23,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true, //note; if we set ANY other properties, we must set this to true.
|
||||
StrokeColor: drawing.ColorRed, // will supercede defaults
|
||||
FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults
|
||||
},
|
||||
|
@ -31,11 +32,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
const style = "svg .background { fill: white; }" +
|
||||
"svg .canvas { fill: white; }" +
|
||||
"svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" +
|
||||
"svg .green.fill.stroke { fill: green; stroke: lightgreen; }" +
|
||||
"svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" +
|
||||
"svg .blue.text { fill: white; }" +
|
||||
"svg .green.text { fill: white; }" +
|
||||
"svg .gray.text { fill: white; }"
|
||||
|
||||
func svgWithCustomInlineCSS(res http.ResponseWriter, _ *http.Request) {
|
||||
res.Header().Set("Content-Type", chart.ContentTypeSVG)
|
||||
|
||||
// Render the CSS with custom css
|
||||
err := pieChart().Render(chart.SVGWithCSS(style, ""), res)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering pie chart: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func svgWithCustomInlineCSSNonce(res http.ResponseWriter, _ *http.Request) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
|
||||
// This should be randomly generated on every request!
|
||||
const nonce = "RAND0MBASE64"
|
||||
|
||||
res.Header().Set("Content-Security-Policy", fmt.Sprintf("style-src 'nonce-%s'", nonce))
|
||||
res.Header().Set("Content-Type", chart.ContentTypeSVG)
|
||||
|
||||
// Render the CSS with custom css and a nonce.
|
||||
// Try changing the nonce to a different string - your browser should block the CSS.
|
||||
err := pieChart().Render(chart.SVGWithCSS(style, nonce), res)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering pie chart: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func svgWithCustomExternalCSS(res http.ResponseWriter, _ *http.Request) {
|
||||
// Add external CSS
|
||||
res.Write([]byte(
|
||||
`<?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))
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
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.
|
||||
|
@ -14,20 +16,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 4.0,
|
||||
},
|
||||
Ticks: []chart.Tick{
|
||||
{0.0, "0.00"},
|
||||
{2.0, "2.00"},
|
||||
{4.0, "4.00"},
|
||||
{6.0, "6.00"},
|
||||
{8.0, "Eight"},
|
||||
{10.0, "Ten"},
|
||||
{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{
|
||||
|
@ -37,12 +36,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
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 |
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
//go:generate go run main.go
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
func main() {
|
||||
|
||||
/*
|
||||
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
|
||||
|
@ -19,18 +21,12 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
graph := chart.Chart{
|
||||
Height: 500,
|
||||
Width: 500,
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
/*Range: &chart.ContinuousRange{
|
||||
Descending: true,
|
||||
},*/
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Descending: true,
|
||||
},
|
||||
|
@ -38,7 +34,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||
},
|
||||
|
@ -48,11 +43,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
f, _ := os.Create("output.png")
|
||||
defer f.Close()
|
||||
graph.Render(chart.PNG, f)
|
||||
}
|