Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
62b1e2c499 | ||
|
8b34cb3bd7 | ||
|
10950a3bf2 | ||
|
d9b5269579 | ||
|
412b25feb4 | ||
|
68cc6a95d3 | ||
|
046daf94fb | ||
|
2c9a9218e5 | ||
|
feef494764 | ||
|
fb0040390c | ||
|
9c65a94050 |
18
.gitignore
vendored
|
@ -1,19 +1 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Other
|
||||
.vscode
|
||||
.DS_Store
|
||||
coverage.html
|
13
.travis.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.6.2
|
||||
|
||||
sudo: false
|
||||
|
||||
before_script:
|
||||
- go get -u github.com/blendlabs/go-assert
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
- go test ./...
|
9
Makefile
Normal file
|
@ -0,0 +1,9 @@
|
|||
all: test
|
||||
|
||||
test:
|
||||
@go test ./...
|
||||
|
||||
cover:
|
||||
@go test -short -covermode=set -coverprofile=profile.cov
|
||||
@go tool cover -html=profile.cov
|
||||
@rm profile.cov
|
99
README.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
# Installation
|
||||
|
||||
To install `chart` run the following:
|
||||
|
||||
```bash
|
||||
> go get -u github.com/wcharczuk/go-chart
|
||||
```
|
||||
|
||||
Most of the components are interchangeable so feel free to crib whatever you want.
|
||||
|
||||
# Output Examples
|
||||
|
||||
Spark Lines:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/tvix_ltm.png)
|
||||
|
||||
Single axis:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/goog_ltm.png)
|
||||
|
||||
Two axis:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/two_axis.png)
|
||||
|
||||
# Other Chart Types
|
||||
|
||||
Pie Chart:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/pie_chart.png)
|
||||
|
||||
The code for this chart can be found in `_examples/pie_chart/main.go`.
|
||||
|
||||
Stacked Bar:
|
||||
|
||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/stacked_bar.png)
|
||||
|
||||
The code for this chart can be found in `_examples/stacked_bar/main.go`.
|
||||
|
||||
# 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.
|
||||
|
||||
# Usage
|
||||
|
||||
Everything starts with the `chart.Chart` object. The bare minimum to draw a chart would be the following:
|
||||
|
||||
```golang
|
||||
|
||||
import (
|
||||
...
|
||||
"bytes"
|
||||
...
|
||||
"github.com/wcharczuk/go-chart" //exposes "chart"
|
||||
)
|
||||
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
err := graph.Render(chart.PNG, buffer)
|
||||
```
|
||||
|
||||
Explanation of the above: A `chart` can have many `Series`, a `Series` is a collection of things that need to be drawn according to the X range and the Y range(s).
|
||||
|
||||
Here, we have a single series with x range values as float64s, rendered to a PNG. Note; we can pass any type of `io.Writer` into `Render(...)`, meaning that we can render the chart to a file or a resonse or anything else that implements `io.Writer`.
|
||||
|
||||
# API Overview
|
||||
|
||||
Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default. One complication here
|
||||
is any object's root `chart.Style` object (i.e named `Style`) and the `Show` property specifically, if any other property is set and the `Show` property is unset, it is assumed to be it's default value of `False`.
|
||||
|
||||
The best way to see the api in action is to look at the examples in the `./_examples/` directory.
|
||||
|
||||
# Design Philosophy
|
||||
|
||||
I wanted to make a charting library that used only native golang, that could be stood up on a server (i.e. it had built in fonts).
|
||||
|
||||
The goal with the API itself is to have the "zero value be useful", and to require the user to not code more than they absolutely needed.
|
||||
|
||||
# Contributions
|
||||
|
||||
This library is super early but contributions are welcome.
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -15,10 +15,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(), //enables / displays the x-axis
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the x-axis
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(), //enables / displays the y-axis
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the y-axis
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
@ -6,23 +6,20 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
sbc := chart.BarChart{
|
||||
Title: "Test Bar Chart",
|
||||
TitleStyle: chart.StyleShow(),
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 40,
|
||||
},
|
||||
},
|
||||
Height: 512,
|
||||
BarWidth: 60,
|
||||
XAxis: chart.StyleShow(),
|
||||
XAxis: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Bars: []chart.Value{
|
||||
{Value: 5.25, Label: "Blue"},
|
||||
|
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
@ -1,88 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
profitStyle := chart.Style{
|
||||
Show: true,
|
||||
FillColor: drawing.ColorFromHex("13c158"),
|
||||
StrokeColor: drawing.ColorFromHex("13c158"),
|
||||
StrokeWidth: 0,
|
||||
}
|
||||
|
||||
lossStyle := chart.Style{
|
||||
Show: true,
|
||||
FillColor: drawing.ColorFromHex("c11313"),
|
||||
StrokeColor: drawing.ColorFromHex("c11313"),
|
||||
StrokeWidth: 0,
|
||||
}
|
||||
|
||||
sbc := chart.BarChart{
|
||||
Title: "Bar Chart Using BaseValue",
|
||||
TitleStyle: chart.StyleShow(),
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 40,
|
||||
},
|
||||
},
|
||||
Height: 512,
|
||||
BarWidth: 60,
|
||||
XAxis: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Ticks: []chart.Tick{
|
||||
{-4.0, "-4"},
|
||||
{-2.0, "-2"},
|
||||
{0, "0"},
|
||||
{2.0, "2"},
|
||||
{4.0, "4"},
|
||||
{6.0, "6"},
|
||||
{8.0, "8"},
|
||||
{10.0, "10"},
|
||||
{12.0, "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!"},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := sbc.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering chart: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func port() string {
|
||||
if len(os.Getenv("PORT")) > 0 {
|
||||
return os.Getenv("PORT")
|
||||
}
|
||||
return "8080"
|
||||
}
|
||||
|
||||
func main() {
|
||||
listenPort := fmt.Sprintf(":%s", port())
|
||||
fmt.Printf("Listening on %s\n", listenPort)
|
||||
http.HandleFunc("/", drawChart)
|
||||
log.Fatal(http.ListenAndServe(listenPort, nil))
|
||||
}
|
Before Width: | Height: | Size: 18 KiB |
|
@ -4,7 +4,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -26,8 +26,8 @@ func drawChartWide(res http.ResponseWriter, req *http.Request) {
|
|||
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},
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,79 +0,0 @@
|
|||
// Usage: http://localhost:8080?series=100&values=1000
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
)
|
||||
|
||||
func random(min, max float64) float64 {
|
||||
return rand.Float64()*(max-min) + min
|
||||
}
|
||||
|
||||
func drawLargeChart(res http.ResponseWriter, r *http.Request) {
|
||||
numSeriesInt64, err := strconv.ParseInt(r.FormValue("series"), 10, 64)
|
||||
if err != nil {
|
||||
numSeriesInt64 = int64(1)
|
||||
}
|
||||
if numSeriesInt64 == 0 {
|
||||
numSeriesInt64 = 1
|
||||
}
|
||||
numSeries := int(numSeriesInt64)
|
||||
|
||||
numValuesInt64, err := strconv.ParseInt(r.FormValue("values"), 10, 64)
|
||||
if err != nil {
|
||||
numValuesInt64 = int64(100)
|
||||
}
|
||||
if numValuesInt64 == 0 {
|
||||
numValuesInt64 = int64(100)
|
||||
}
|
||||
numValues := int(numValuesInt64)
|
||||
|
||||
series := make([]chart.Series, numSeries)
|
||||
|
||||
for i := 0; i < numSeries; i++ {
|
||||
xValues := make([]time.Time, numValues)
|
||||
yValues := make([]float64, numValues)
|
||||
|
||||
for j := 0; j < numValues; j++ {
|
||||
xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1)
|
||||
yValues[j] = random(float64(-500), float64(500))
|
||||
}
|
||||
|
||||
series[i] = chart.TimeSeries{
|
||||
Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i),
|
||||
XValues: xValues,
|
||||
YValues: yValues,
|
||||
}
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Name: "Time",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "Value",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: series,
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawLargeChart)
|
||||
http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Write([]byte{})
|
||||
})
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Before Width: | Height: | Size: 68 KiB |
|
@ -1,58 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
)
|
||||
|
||||
// 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{
|
||||
// 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"}},
|
||||
},
|
||||
}
|
||||
|
||||
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 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; }"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", inlineSVGWithClasses)
|
||||
http.HandleFunc("/main.css", css)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -16,7 +16,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
ValueFormatter: func(v interface{}) string {
|
||||
if vf, isFloat := v.(float64); isFloat {
|
||||
return fmt.Sprintf("%0.6f", vf)
|
||||
|
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
@ -3,9 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -20,15 +19,19 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
FillColor: drawing.ColorFromHex("efefef"),
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: seq.Range(1.0, 100.0),
|
||||
YValues: seq.RandomValuesWithMax(100, 512),
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -43,15 +46,19 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) {
|
|||
FillColor: drawing.ColorFromHex("efefef"),
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: seq.Range(1.0, 100.0),
|
||||
YValues: seq.RandomValuesWithMax(100, 512),
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 64 KiB |
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -14,7 +14,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 10.0,
|
||||
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
@ -3,8 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 987 B |
|
@ -1,87 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashworks/go-chart"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const style = "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; }"
|
||||
|
||||
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))
|
||||
}
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -14,7 +14,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 4.0,
|
||||
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -20,13 +20,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
Height: 500,
|
||||
Width: 500,
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
/*Range: &chart.ContinuousRange{
|
||||
Descending: true,
|
||||
},*/
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Range: &chart.ContinuousRange{
|
||||
Descending: true,
|
||||
},
|
||||
|
|
Before Width: | Height: | Size: 19 KiB |
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -16,10 +16,10 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
},
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -16,10 +16,10 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
},
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
|
|
|
@ -3,21 +3,20 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"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 `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
|
||||
InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
|
||||
*/
|
||||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
// note we create a LinearRegressionSeries series by assignin the inner series.
|
||||
|
|
44
_examples/market_hours/main.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
start := chart.Date.Date(2016, 7, 01, chart.Date.Eastern())
|
||||
end := chart.Date.Date(2016, 07, 21, chart.Date.Eastern())
|
||||
xv := chart.Sequence.MarketHours(start, end, chart.NYSEOpen, chart.NYSEClose, chart.Date.IsNYSEHoliday)
|
||||
yv := chart.Sequence.RandomWithAverage(len(xv), 200, 10)
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
TickPosition: chart.TickPositionBetweenTicks,
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
Range: &chart.MarketHoursRange{
|
||||
MarketOpen: chart.NYSEOpen,
|
||||
MarketClose: chart.NYSEClose,
|
||||
HolidayProvider: chart.Date.IsNYSEHoliday,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.TimeSeries{
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
BIN
_examples/market_hours/output.png
Normal file
After Width: | Height: | Size: 67 KiB |
|
@ -3,15 +3,14 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(1.0, 100.0),
|
||||
YValues: seq.New(seq.NewRandom().WithLen(100).WithMax(150).WithMin(50)).Array(),
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.RandomWithAverage(100, 100, 50),
|
||||
}
|
||||
|
||||
minSeries := &chart.MinSeries{
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -30,26 +30,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func drawChartRegression(res http.ResponseWriter, req *http.Request) {
|
||||
pie := chart.PieChart{
|
||||
Width: 512,
|
||||
Height: 512,
|
||||
Values: []chart.Value{
|
||||
{Value: 5, Label: "Blue"},
|
||||
{Value: 2, Label: "Two"},
|
||||
{Value: 1, Label: "One"},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", chart.ContentTypeSVG)
|
||||
err := pie.Render(chart.SVG, res)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering pie chart: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.HandleFunc("/reg", drawChartRegression)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
@ -1,17 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">
|
||||
<path d="M 256 256
|
||||
L 507 256
|
||||
A 251 251 225.00 1 1 79 79
|
||||
L 256 256
|
||||
Z" style="stroke-width:5;stroke:rgba(255,255,255,1.0);fill:rgba(106,195,203,1.0)"/>
|
||||
<path d="M 256 256
|
||||
L 79 79
|
||||
A 251 251 90.00 0 1 433 79
|
||||
L 256 256
|
||||
Z" style="stroke-width:5;stroke:rgba(255,255,255,1.0);fill:rgba(42,190,137,1.0)"/>
|
||||
<path d="M 256 256
|
||||
L 433 79
|
||||
A 251 251 45.00 0 1 507 256
|
||||
L 256 256
|
||||
Z" style="stroke-width:5;stroke:rgba(255,255,255,1.0);fill:rgba(110,128,139,1.0)"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 565 B |
|
@ -1,42 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
)
|
||||
|
||||
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 `ValuesProvider`, so really you could chain `PolynomialRegressionSeries` together if you wanted.
|
||||
*/
|
||||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
Before Width: | Height: | Size: 54 KiB |
|
@ -7,8 +7,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func parseInt(str string) int {
|
||||
|
@ -24,7 +23,7 @@ func parseFloat64(str string) float64 {
|
|||
func readData() ([]time.Time, []float64) {
|
||||
var xvalues []time.Time
|
||||
var yvalues []float64
|
||||
err := util.File.ReadByLines("requests.csv", func(line string) error {
|
||||
err := chart.File.ReadByLines("requests.csv", func(line string) {
|
||||
parts := strings.Split(line, ",")
|
||||
year := parseInt(parts[0])
|
||||
month := parseInt(parts[1])
|
||||
|
@ -33,7 +32,6 @@ func readData() ([]time.Time, []float64) {
|
|||
elapsedMillis := parseFloat64(parts[4])
|
||||
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
||||
yvalues = append(yvalues, elapsedMillis)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
|
@ -43,12 +41,12 @@ func readData() ([]time.Time, []float64) {
|
|||
|
||||
func releases() []chart.GridLine {
|
||||
return []chart.GridLine{
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))},
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
|
||||
{Value: util.Time.ToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
|
||||
{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))},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +103,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
GridMajorStyle: chart.Style{
|
||||
Show: true,
|
||||
|
@ -125,7 +125,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
|
||||
|
||||
res.Header().Set("Content-Type", chart.ContentTypePNG)
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/util"
|
||||
|
||||
chart "git.fireandbrimst.one/aw/go-chart"
|
||||
)
|
||||
|
||||
var lock sync.Mutex
|
||||
var graph *chart.Chart
|
||||
var ts *chart.TimeSeries
|
||||
|
||||
func addData(t time.Time, e time.Duration) {
|
||||
lock.Lock()
|
||||
ts.XValues = append(ts.XValues, t)
|
||||
ts.YValues = append(ts.YValues, util.Time.Millis(e))
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
addData(start, time.Since(start))
|
||||
}()
|
||||
if len(ts.XValues) == 0 {
|
||||
http.Error(res, "no data (yet)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
if err := graph.Render(chart.PNG, res); err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ts = &chart.TimeSeries{
|
||||
XValues: []time.Time{},
|
||||
YValues: []float64{},
|
||||
}
|
||||
graph = &chart.Chart{
|
||||
Series: []chart.Series{ts},
|
||||
}
|
||||
http.HandleFunc("/", drawChart)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -4,39 +4,48 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
||||
viridisByY := func(xr, yr chart.Range, index int, x, y float64) drawing.Color {
|
||||
return chart.Viridis(y, yr.GetMin(), yr.GetMax())
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeWidth: chart.Disabled,
|
||||
DotWidth: 5,
|
||||
DotColorProvider: viridisByY,
|
||||
Show: true,
|
||||
StrokeWidth: chart.Disabled,
|
||||
DotWidth: 3,
|
||||
},
|
||||
XValues: seq.Range(0, 127),
|
||||
YValues: seq.New(seq.NewRandom().WithLen(128).WithMax(1024)).Array(),
|
||||
XValues: chart.Sequence.Random(32, 1024),
|
||||
YValues: chart.Sequence.Random(32, 1024),
|
||||
},
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeWidth: chart.Disabled,
|
||||
DotWidth: 5,
|
||||
},
|
||||
XValues: chart.Sequence.Random(16, 1024),
|
||||
YValues: chart.Sequence.Random(16, 1024),
|
||||
},
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeWidth: chart.Disabled,
|
||||
DotWidth: 7,
|
||||
},
|
||||
XValues: chart.Sequence.Random(8, 1024),
|
||||
YValues: chart.Sequence.Random(8, 1024),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", chart.ContentTypePNG)
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := graph.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func unit(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -44,20 +53,20 @@ func unit(res http.ResponseWriter, req *http.Request) {
|
|||
Height: 50,
|
||||
Width: 50,
|
||||
Canvas: chart.Style{
|
||||
Padding: chart.BoxZero,
|
||||
Padding: chart.Box{IsSet: true},
|
||||
},
|
||||
Background: chart.Style{
|
||||
Padding: chart.BoxZero,
|
||||
Padding: chart.Box{IsSet: true},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: seq.RangeWithStep(0, 4, 1),
|
||||
YValues: seq.RangeWithStep(0, 4, 1),
|
||||
XValues: chart.Sequence.Float64(0, 4, 1),
|
||||
YValues: chart.Sequence.Float64(0, 4, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", chart.ContentTypePNG)
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := graph.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
|
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
|
@ -3,16 +3,20 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
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.
|
||||
|
|
|
@ -5,21 +5,18 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
sbc := chart.StackedBarChart{
|
||||
Title: "Test Stacked Bar Chart",
|
||||
TitleStyle: chart.StyleShow(),
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 40,
|
||||
},
|
||||
},
|
||||
Height: 512,
|
||||
XAxis: chart.StyleShow(),
|
||||
YAxis: chart.StyleShow(),
|
||||
XAxis: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
YAxis: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
Bars: []chart.StackedBar{
|
||||
{
|
||||
Name: "This is a very long string to test word break wrapping.",
|
||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
@ -4,8 +4,8 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -43,11 +43,11 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
TickPosition: chart.TickPositionBetweenTicks,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{Show: true},
|
||||
Range: &chart.ContinuousRange{
|
||||
Max: 220.0,
|
||||
Min: 180.0,
|
||||
|
|
|
@ -3,8 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
Before Width: | Height: | Size: 8.6 KiB |
|
@ -4,17 +4,19 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
/*
|
||||
This is an example of using the `TimeSeries` to automatically coerce time.Time values into a continuous xrange.
|
||||
Note: chart.TimeSeries implements `ValueFormatterProvider` and as a result gives the XAxis the appropriate formatter to use for the ticks.
|
||||
Note: chart.TimeSeries implements `ValueFormatterProvider` and as a result gives the XAxis the appropariate formatter to use for the ticks.
|
||||
*/
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.TimeSeries{
|
||||
|
@ -46,7 +48,9 @@ func drawCustomChart(res http.ResponseWriter, req *http.Request) {
|
|||
*/
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
},
|
||||
Series: []chart.Series{
|
||||
|
@ -75,7 +79,7 @@ func drawCustomChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.HandleFunc("/favicon.ico", func(res http.ResponseWriter, req *http.Request) {
|
||||
http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Write([]byte{})
|
||||
})
|
||||
http.HandleFunc("/custom", drawCustomChart)
|
||||
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -4,8 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -18,19 +17,25 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(), //enables / displays the x-axis
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the x-axis
|
||||
},
|
||||
TickPosition: chart.TickPositionBetweenTicks,
|
||||
ValueFormatter: func(v interface{}) string {
|
||||
typed := v.(float64)
|
||||
typedDate := util.Time.FromFloat64(typed)
|
||||
typedDate := chart.Time.FromFloat64(typed)
|
||||
return fmt.Sprintf("%d-%d\n%d", typedDate.Month(), typedDate.Day(), typedDate.Year())
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(), //enables / displays the y-axis
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the y-axis
|
||||
},
|
||||
},
|
||||
YAxisSecondary: chart.YAxis{
|
||||
Style: chart.StyleShow(), //enables / displays the secondary y-axis
|
||||
Style: chart.Style{
|
||||
Show: true, //enables / displays the secondary y-axis
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
|
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
@ -4,8 +4,8 @@ import (
|
|||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
//"time"
|
||||
"github.com/wcharczuk/go-chart" //exposes "chart"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -14,8 +14,12 @@ func main() {
|
|||
b = 1000
|
||||
|
||||
ts1 := chart.ContinuousSeries{ //TimeSeries{
|
||||
Name: "Time Series",
|
||||
Style: chart.StyleShow(),
|
||||
Name: "Time Series",
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
|
||||
//XValues: []time.Time{time.Unix(3*b,0),time.Unix(4*b,0),time.Unix(5*b,0),time.Unix(6*b,0),time.Unix(7*b,0),time.Unix(8*b,0),time.Unix(9*b,0),time.Unix(10*b,0)},
|
||||
XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b},
|
||||
YValues: []float64{1.0, 2.0, 30.0, 4.0, 50.0, 6.0, 7.0, 88.0},
|
||||
}
|
||||
|
|
|
@ -3,13 +3,6 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*AnnotationSeries)(nil)
|
||||
)
|
||||
|
||||
// AnnotationSeries is a series of labels on the chart.
|
||||
|
@ -62,10 +55,10 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
|||
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 = util.Math.MinInt(box.Top, ab.Top)
|
||||
box.Left = util.Math.MinInt(box.Left, ab.Left)
|
||||
box.Right = util.Math.MaxInt(box.Right, ab.Right)
|
||||
box.Bottom = util.Math.MaxInt(box.Bottom, ab.Bottom)
|
||||
box.Top = Math.MinInt(box.Top, ab.Top)
|
||||
box.Left = Math.MinInt(box.Left, ab.Left)
|
||||
box.Right = Math.MaxInt(box.Right, ab.Right)
|
||||
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
|
||||
}
|
||||
}
|
||||
return box
|
||||
|
|
119
annotation_series_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
as := AnnotationSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
Annotations: []Value2{
|
||||
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := PNG(110, 110)
|
||||
assert.Nil(err)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
|
||||
xrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
Max: 4.0,
|
||||
Domain: 100,
|
||||
}
|
||||
yrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
Max: 4.0,
|
||||
Domain: 100,
|
||||
}
|
||||
|
||||
cb := Box{
|
||||
Top: 5,
|
||||
Left: 5,
|
||||
Right: 105,
|
||||
Bottom: 105,
|
||||
}
|
||||
sd := Style{
|
||||
FontSize: 10.0,
|
||||
Font: f,
|
||||
}
|
||||
|
||||
box := as.Measure(r, cb, xrange, yrange, sd)
|
||||
assert.False(box.IsZero())
|
||||
assert.Equal(-5.0, box.Top)
|
||||
assert.Equal(5.0, box.Left)
|
||||
assert.Equal(146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px.
|
||||
assert.Equal(115.0, box.Bottom)
|
||||
}
|
||||
|
||||
func TestAnnotationSeriesRender(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
as := AnnotationSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
FillColor: drawing.ColorWhite,
|
||||
StrokeColor: drawing.ColorBlack,
|
||||
},
|
||||
Annotations: []Value2{
|
||||
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := PNG(110, 110)
|
||||
assert.Nil(err)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
|
||||
xrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
Max: 4.0,
|
||||
Domain: 100,
|
||||
}
|
||||
yrange := &ContinuousRange{
|
||||
Min: 1.0,
|
||||
Max: 4.0,
|
||||
Domain: 100,
|
||||
}
|
||||
|
||||
cb := Box{
|
||||
Top: 5,
|
||||
Left: 5,
|
||||
Right: 105,
|
||||
Bottom: 105,
|
||||
}
|
||||
sd := Style{
|
||||
FontSize: 10.0,
|
||||
Font: f,
|
||||
}
|
||||
|
||||
as.Render(r, cb, xrange, yrange, sd)
|
||||
|
||||
rr, isRaster := r.(*rasterRenderer)
|
||||
assert.True(isRaster)
|
||||
assert.NotNil(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)
|
||||
}
|
90
bar_chart.go
|
@ -6,8 +6,7 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// BarChart is a chart that draws bars on a range.
|
||||
|
@ -15,8 +14,6 @@ type BarChart struct {
|
|||
Title string
|
||||
TitleStyle Style
|
||||
|
||||
ColorPalette ColorPalette
|
||||
|
||||
Width int
|
||||
Height int
|
||||
DPI float64
|
||||
|
@ -31,9 +28,6 @@ type BarChart struct {
|
|||
|
||||
BarSpacing int
|
||||
|
||||
UseBaseValue bool
|
||||
BaseValue float64
|
||||
|
||||
Font *truetype.Font
|
||||
defaultFont *truetype.Font
|
||||
|
||||
|
@ -129,7 +123,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
|
||||
yr = bc.setRangeDomains(canvasBox, yr)
|
||||
}
|
||||
bc.drawCanvas(r, canvasBox)
|
||||
|
||||
bc.drawBars(r, canvasBox, yr)
|
||||
bc.drawXAxis(r, canvasBox)
|
||||
bc.drawYAxis(r, canvasBox, yr, yt)
|
||||
|
@ -142,10 +136,6 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
return r.Save(w)
|
||||
}
|
||||
|
||||
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||
Draw.Box(r, canvasBox, bc.getCanvasStyle())
|
||||
}
|
||||
|
||||
func (bc BarChart) getRanges() Range {
|
||||
var yrange Range
|
||||
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
|
||||
|
@ -202,20 +192,11 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
|||
|
||||
by = canvasBox.Bottom - yr.Translate(bar.Value)
|
||||
|
||||
if bc.UseBaseValue {
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
|
||||
}
|
||||
} else {
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom,
|
||||
}
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom,
|
||||
}
|
||||
|
||||
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
|
||||
|
@ -296,32 +277,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
|||
|
||||
func (bc BarChart) drawTitle(r Renderer) {
|
||||
if len(bc.Title) > 0 && bc.TitleStyle.Show {
|
||||
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,
|
||||
Draw.TextWithin(r, bc.Title, bc.box(), bc.styleDefaultsTitle())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -410,7 +366,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
|||
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||
|
||||
xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
xaxisHeight = Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -438,8 +394,8 @@ func (bc BarChart) box() Box {
|
|||
dpb := bc.Background.Padding.GetBottom(50)
|
||||
|
||||
return Box{
|
||||
Top: bc.Background.Padding.GetTop(20),
|
||||
Left: bc.Background.Padding.GetLeft(20),
|
||||
Top: 20,
|
||||
Left: 20,
|
||||
Right: bc.GetWidth() - dpr,
|
||||
Bottom: bc.GetHeight() - dpb,
|
||||
}
|
||||
|
@ -451,23 +407,23 @@ func (bc BarChart) getBackgroundStyle() Style {
|
|||
|
||||
func (bc BarChart) styleDefaultsBackground() Style {
|
||||
return Style{
|
||||
FillColor: bc.GetColorPalette().BackgroundColor(),
|
||||
StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
|
||||
FillColor: DefaultBackgroundColor,
|
||||
StrokeColor: DefaultBackgroundStrokeColor,
|
||||
StrokeWidth: DefaultStrokeWidth,
|
||||
}
|
||||
}
|
||||
|
||||
func (bc BarChart) styleDefaultsBar(index int) Style {
|
||||
return Style{
|
||||
StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||
StrokeColor: GetAlternateColor(index),
|
||||
StrokeWidth: 3.0,
|
||||
FillColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||
FillColor: GetAlternateColor(index),
|
||||
}
|
||||
}
|
||||
|
||||
func (bc BarChart) styleDefaultsTitle() Style {
|
||||
return bc.TitleStyle.InheritFrom(Style{
|
||||
FontColor: bc.GetColorPalette().TextColor(),
|
||||
FontColor: DefaultTextColor,
|
||||
Font: bc.GetFont(),
|
||||
FontSize: bc.getTitleFontSize(),
|
||||
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||
|
@ -477,7 +433,7 @@ func (bc BarChart) styleDefaultsTitle() Style {
|
|||
}
|
||||
|
||||
func (bc BarChart) getTitleFontSize() float64 {
|
||||
effectiveDimension := util.Math.MinInt(bc.GetWidth(), bc.GetHeight())
|
||||
effectiveDimension := Math.MinInt(bc.GetWidth(), bc.GetHeight())
|
||||
if effectiveDimension >= 2048 {
|
||||
return 48
|
||||
} else if effectiveDimension >= 1024 {
|
||||
|
@ -492,10 +448,10 @@ func (bc BarChart) getTitleFontSize() float64 {
|
|||
|
||||
func (bc BarChart) styleDefaultsAxes() Style {
|
||||
return Style{
|
||||
StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
|
||||
StrokeColor: DefaultAxisColor,
|
||||
Font: bc.GetFont(),
|
||||
FontSize: DefaultAxisFontSize,
|
||||
FontColor: bc.GetColorPalette().TextColor(),
|
||||
FontColor: DefaultAxisColor,
|
||||
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||
TextVerticalAlign: TextVerticalAlignTop,
|
||||
TextWrap: TextWrapWord,
|
||||
|
@ -507,11 +463,3 @@ func (bc BarChart) styleDefaultsElements() Style {
|
|||
Font: bc.GetFont(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetColorPalette returns the color palette for the chart.
|
||||
func (bc BarChart) GetColorPalette() ColorPalette {
|
||||
if bc.ColorPalette != nil {
|
||||
return bc.ColorPalette
|
||||
}
|
||||
return AlternateColorPalette
|
||||
}
|
||||
|
|
320
bar_chart_test.go
Normal file
|
@ -0,0 +1,320 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestBarChartRender(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
TitleStyle: StyleShow(),
|
||||
XAxis: StyleShow(),
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Bars: []Value{
|
||||
{Value: 1.0, Label: "One"},
|
||||
{Value: 2.0, Label: "Two"},
|
||||
{Value: 3.0, Label: "Three"},
|
||||
{Value: 4.0, Label: "Four"},
|
||||
{Value: 5.0, Label: "Five"},
|
||||
},
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := bc.Render(PNG, buf)
|
||||
assert.Nil(err)
|
||||
assert.NotZero(buf.Len())
|
||||
}
|
||||
|
||||
func TestBarChartRenderZero(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
Title: "Test Title",
|
||||
TitleStyle: StyleShow(),
|
||||
XAxis: StyleShow(),
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Bars: []Value{
|
||||
{Value: 0.0, Label: "One"},
|
||||
{Value: 0.0, Label: "Two"},
|
||||
},
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := bc.Render(PNG, buf)
|
||||
assert.NotNil(err)
|
||||
}
|
||||
|
||||
func TestBarChartProps(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
|
||||
assert.Equal(DefaultDPI, bc.GetDPI())
|
||||
bc.DPI = 100
|
||||
assert.Equal(100, bc.GetDPI())
|
||||
|
||||
assert.Nil(bc.GetFont())
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
bc.Font = f
|
||||
assert.NotNil(bc.GetFont())
|
||||
|
||||
assert.Equal(DefaultChartWidth, bc.GetWidth())
|
||||
bc.Width = DefaultChartWidth - 1
|
||||
assert.Equal(DefaultChartWidth-1, bc.GetWidth())
|
||||
|
||||
assert.Equal(DefaultChartHeight, bc.GetHeight())
|
||||
bc.Height = DefaultChartHeight - 1
|
||||
assert.Equal(DefaultChartHeight-1, bc.GetHeight())
|
||||
|
||||
assert.Equal(DefaultBarSpacing, bc.GetBarSpacing())
|
||||
bc.BarSpacing = 150
|
||||
assert.Equal(150, bc.GetBarSpacing())
|
||||
|
||||
assert.Equal(DefaultBarWidth, bc.GetBarWidth())
|
||||
bc.BarWidth = 75
|
||||
assert.Equal(75, bc.GetBarWidth())
|
||||
}
|
||||
|
||||
func TestBarChartRenderNoBars(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
|
||||
assert.NotNil(err)
|
||||
}
|
||||
|
||||
func TestBarChartGetRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
|
||||
assert.Equal(-math.MaxFloat64, yr.GetMax())
|
||||
assert.Equal(math.MaxFloat64, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesBarsMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Bars: []Value{
|
||||
{Value: 1.0},
|
||||
{Value: 10.0},
|
||||
},
|
||||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
|
||||
assert.Equal(10, yr.GetMax())
|
||||
assert.Equal(1, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
YAxis: YAxis{
|
||||
Range: &ContinuousRange{
|
||||
Min: 5.0,
|
||||
Max: 15.0,
|
||||
},
|
||||
Ticks: []Tick{
|
||||
{Value: 7.0, Label: "Foo"},
|
||||
{Value: 11.0, Label: "Foo2"},
|
||||
},
|
||||
},
|
||||
Bars: []Value{
|
||||
{Value: 1.0},
|
||||
{Value: 10.0},
|
||||
},
|
||||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
|
||||
assert.Equal(15, yr.GetMax())
|
||||
assert.Equal(5, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartGetRangesTicksMinMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
YAxis: YAxis{
|
||||
Ticks: []Tick{
|
||||
{Value: 7.0, Label: "Foo"},
|
||||
{Value: 11.0, Label: "Foo2"},
|
||||
},
|
||||
},
|
||||
Bars: []Value{
|
||||
{Value: 1.0},
|
||||
{Value: 10.0},
|
||||
},
|
||||
}
|
||||
|
||||
yr := bc.getRanges()
|
||||
assert.NotNil(yr)
|
||||
assert.False(yr.IsZero())
|
||||
|
||||
assert.Equal(11, yr.GetMax())
|
||||
assert.Equal(7, yr.GetMin())
|
||||
}
|
||||
|
||||
func TestBarChartHasAxes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
assert.False(bc.hasAxes())
|
||||
bc.YAxis = YAxis{
|
||||
Style: StyleShow(),
|
||||
}
|
||||
|
||||
assert.True(bc.hasAxes())
|
||||
}
|
||||
|
||||
func TestBarChartGetDefaultCanvasBox(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
b := bc.getDefaultCanvasBox()
|
||||
assert.False(b.IsZero())
|
||||
}
|
||||
|
||||
func TestBarChartSetRangeDomains(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
cb := bc.box()
|
||||
yr := bc.getRanges()
|
||||
yr2 := bc.setRangeDomains(cb, yr)
|
||||
assert.NotZero(yr2.GetDomain())
|
||||
}
|
||||
|
||||
func TestBarChartGetValueFormatters(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{}
|
||||
vf := bc.getValueFormatters()
|
||||
assert.NotNil(vf)
|
||||
assert.Equal("1234.00", vf(1234.0))
|
||||
|
||||
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
|
||||
assert.Equal("test", bc.getValueFormatters()(1234))
|
||||
}
|
||||
|
||||
func TestBarChartGetAxesTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Bars: []Value{
|
||||
{Value: 1.0},
|
||||
{Value: 2.0},
|
||||
{Value: 3.0},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := PNG(128, 128)
|
||||
assert.Nil(err)
|
||||
yr := bc.getRanges()
|
||||
yf := bc.getValueFormatters()
|
||||
|
||||
ticks := bc.getAxesTicks(r, yr, yf)
|
||||
assert.Empty(ticks)
|
||||
|
||||
bc.YAxis.Style.Show = true
|
||||
ticks = bc.getAxesTicks(r, yr, yf)
|
||||
assert.Len(ticks, 2)
|
||||
}
|
||||
|
||||
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
BarWidth: 10,
|
||||
Bars: []Value{
|
||||
{Value: 1.0, Label: "One"},
|
||||
{Value: 2.0, Label: "Two"},
|
||||
{Value: 3.0, Label: "Three"},
|
||||
{Value: 4.0, Label: "Four"},
|
||||
{Value: 5.0, Label: "Five"},
|
||||
},
|
||||
}
|
||||
|
||||
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.NotZero(spacing)
|
||||
|
||||
bc.BarWidth = 250
|
||||
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.Zero(spacing)
|
||||
}
|
||||
|
||||
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BarChart{
|
||||
Width: 1024,
|
||||
BarWidth: 10,
|
||||
Bars: []Value{
|
||||
{Value: 1.0, Label: "One"},
|
||||
{Value: 2.0, Label: "Two"},
|
||||
{Value: 3.0, Label: "Three"},
|
||||
{Value: 4.0, Label: "Four"},
|
||||
{Value: 5.0, Label: "Five"},
|
||||
},
|
||||
}
|
||||
|
||||
cb := bc.box()
|
||||
|
||||
spacing := bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.NotZero(spacing)
|
||||
|
||||
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||
assert.Equal(10, barWidth)
|
||||
|
||||
bc.BarWidth = 250
|
||||
spacing = bc.calculateEffectiveBarSpacing(bc.box())
|
||||
assert.Zero(spacing)
|
||||
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
|
||||
assert.Equal(199, barWidth)
|
||||
|
||||
assert.Equal(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)
|
||||
}
|
||||
|
||||
func TestBarChatGetTitleFontSize(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
|
||||
assert.Equal(48, size)
|
||||
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
|
||||
assert.Equal(24, size)
|
||||
size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
|
||||
assert.Equal(18, size)
|
||||
size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
|
||||
assert.Equal(12, size)
|
||||
size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
|
||||
assert.Equal(10, size)
|
||||
}
|
|
@ -2,13 +2,7 @@ package chart
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*BollingerBandsSeries)(nil)
|
||||
"math"
|
||||
)
|
||||
|
||||
// BollingerBandsSeries draws bollinger bands for an inner series.
|
||||
|
@ -20,9 +14,9 @@ type BollingerBandsSeries struct {
|
|||
|
||||
Period int
|
||||
K float64
|
||||
InnerSeries ValuesProvider
|
||||
InnerSeries ValueProvider
|
||||
|
||||
valueBuffer *seq.Buffer
|
||||
valueBuffer *RingBuffer
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
|
@ -48,9 +42,7 @@ func (bbs BollingerBandsSeries) GetPeriod() int {
|
|||
return bbs.Period
|
||||
}
|
||||
|
||||
// GetK returns the K value, or the number of standard deviations above and below
|
||||
// to band the simple moving average with.
|
||||
// Typical K value is 2.0.
|
||||
// GetK returns the K value.
|
||||
func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
|
||||
if bbs.K == 0 {
|
||||
if len(defaults) > 0 {
|
||||
|
@ -62,35 +54,35 @@ func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
|
|||
}
|
||||
|
||||
// Len returns the number of elements in the series.
|
||||
func (bbs BollingerBandsSeries) Len() int {
|
||||
func (bbs *BollingerBandsSeries) Len() int {
|
||||
return bbs.InnerSeries.Len()
|
||||
}
|
||||
|
||||
// GetBoundedValues gets the bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
|
||||
// GetBoundedValue gets the bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedValue(index int) (x, y1, y2 float64) {
|
||||
if bbs.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
if bbs.valueBuffer == nil || index == 0 {
|
||||
bbs.valueBuffer = seq.NewBufferWithCapacity(bbs.GetPeriod())
|
||||
bbs.valueBuffer = NewRingBufferWithCapacity(bbs.GetPeriod())
|
||||
}
|
||||
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
|
||||
bbs.valueBuffer.Dequeue()
|
||||
}
|
||||
px, py := bbs.InnerSeries.GetValues(index)
|
||||
px, py := bbs.InnerSeries.GetValue(index)
|
||||
bbs.valueBuffer.Enqueue(py)
|
||||
x = px
|
||||
|
||||
ay := seq.New(bbs.valueBuffer).Average()
|
||||
std := seq.New(bbs.valueBuffer).StdDev()
|
||||
ay := bbs.getAverage(bbs.valueBuffer)
|
||||
std := bbs.getStdDev(bbs.valueBuffer)
|
||||
|
||||
y1 = ay + (bbs.GetK() * std)
|
||||
y2 = ay - (bbs.GetK() * std)
|
||||
return
|
||||
}
|
||||
|
||||
// GetBoundedLastValues returns the last bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
|
||||
// GetBoundedLastValue returns the last bounded value for the series.
|
||||
func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) {
|
||||
if bbs.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
|
@ -101,15 +93,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
|
|||
startAt = 0
|
||||
}
|
||||
|
||||
vb := seq.NewBufferWithCapacity(period)
|
||||
vb := NewRingBufferWithCapacity(period)
|
||||
for index := startAt; index < seriesLength; index++ {
|
||||
xn, yn := bbs.InnerSeries.GetValues(index)
|
||||
xn, yn := bbs.InnerSeries.GetValue(index)
|
||||
vb.Enqueue(yn)
|
||||
x = xn
|
||||
}
|
||||
|
||||
ay := seq.Seq{Provider: vb}.Average()
|
||||
std := seq.Seq{Provider: vb}.StdDev()
|
||||
ay := bbs.getAverage(vb)
|
||||
std := bbs.getStdDev(vb)
|
||||
|
||||
y1 = ay + (bbs.GetK() * std)
|
||||
y2 = ay - (bbs.GetK() * std)
|
||||
|
@ -128,6 +120,37 @@ func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrang
|
|||
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {
|
||||
var accum float64
|
||||
valueBuffer.Each(func(v interface{}) {
|
||||
if typed, isTyped := v.(float64); isTyped {
|
||||
accum += typed
|
||||
}
|
||||
})
|
||||
return accum / float64(valueBuffer.Len())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 {
|
||||
if valueBuffer.Len() == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var variance float64
|
||||
m := bbs.getAverage(valueBuffer)
|
||||
|
||||
valueBuffer.Each(func(v interface{}) {
|
||||
if n, isTyped := v.(float64); isTyped {
|
||||
variance += (float64(n) - m) * (float64(n) - m)
|
||||
}
|
||||
})
|
||||
|
||||
return variance / float64(valueBuffer.Len())
|
||||
}
|
||||
|
||||
func (bbs BollingerBandsSeries) getStdDev(valueBuffer *RingBuffer) float64 {
|
||||
return math.Pow(bbs.getVariance(valueBuffer), 0.5)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (bbs BollingerBandsSeries) Validate() error {
|
||||
if bbs.InnerSeries == nil {
|
||||
|
|
51
bollinger_band_series_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestBollingerBandSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Random(100, 1024),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
InnerSeries: s1,
|
||||
}
|
||||
|
||||
xvalues := make([]float64, 100)
|
||||
y1values := make([]float64, 100)
|
||||
y2values := make([]float64, 100)
|
||||
|
||||
for x := 0; x < 100; x++ {
|
||||
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValue(x)
|
||||
}
|
||||
|
||||
for x := bbs.GetPeriod(); x < 100; x++ {
|
||||
assert.True(y1values[x] > y2values[x])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBollingerBandLastValue(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Float64(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))
|
||||
}
|
56
box.go
|
@ -3,8 +3,6 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -91,12 +89,12 @@ func (b Box) GetBottom(defaults ...int) int {
|
|||
|
||||
// Width returns the width
|
||||
func (b Box) Width() int {
|
||||
return util.Math.AbsInt(b.Right - b.Left)
|
||||
return Math.AbsInt(b.Right - b.Left)
|
||||
}
|
||||
|
||||
// Height returns the height
|
||||
func (b Box) Height() int {
|
||||
return util.Math.AbsInt(b.Bottom - b.Top)
|
||||
return Math.AbsInt(b.Bottom - b.Top)
|
||||
}
|
||||
|
||||
// Center returns the center of the box
|
||||
|
@ -148,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: util.Math.MinInt(b.Top, other.Top),
|
||||
Left: util.Math.MinInt(b.Left, other.Left),
|
||||
Right: util.Math.MaxInt(b.Right, other.Right),
|
||||
Bottom: util.Math.MaxInt(b.Bottom, other.Bottom),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,10 +220,10 @@ func (b Box) Fit(other Box) Box {
|
|||
func (b Box) Constrain(other Box) Box {
|
||||
newBox := b.Clone()
|
||||
|
||||
newBox.Top = util.Math.MaxInt(newBox.Top, other.Top)
|
||||
newBox.Left = util.Math.MaxInt(newBox.Left, other.Left)
|
||||
newBox.Right = util.Math.MinInt(newBox.Right, other.Right)
|
||||
newBox.Bottom = util.Math.MinInt(newBox.Bottom, other.Bottom)
|
||||
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
|
||||
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
|
||||
newBox.Right = Math.MinInt(newBox.Right, other.Right)
|
||||
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
|
||||
|
||||
return newBox
|
||||
}
|
||||
|
@ -264,36 +262,36 @@ type BoxCorners struct {
|
|||
// Box return the BoxCorners as a regular box.
|
||||
func (bc BoxCorners) Box() Box {
|
||||
return Box{
|
||||
Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
||||
Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
||||
Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
||||
Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
||||
Top: Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
||||
Left: Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
||||
Right: Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
||||
Bottom: Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// Width returns the width
|
||||
func (bc BoxCorners) Width() int {
|
||||
minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
minLeft := Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
maxRight := Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
return maxRight - minLeft
|
||||
}
|
||||
|
||||
// Height returns the height
|
||||
func (bc BoxCorners) Height() int {
|
||||
minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
minTop := Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
maxBottom := Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
return maxBottom - minTop
|
||||
}
|
||||
|
||||
// Center returns the center of the box
|
||||
func (bc BoxCorners) Center() (x, y int) {
|
||||
|
||||
left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
left := Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
right := Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||
x = ((right - left) >> 1) + left
|
||||
|
||||
top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
top := Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
bottom := Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
y = ((bottom - top) >> 1) + top
|
||||
|
||||
return
|
||||
|
@ -303,12 +301,12 @@ func (bc BoxCorners) Center() (x, y int) {
|
|||
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
|
||||
cx, cy := bc.Center()
|
||||
|
||||
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
|
||||
thetaRadians := Math.DegreesToRadians(thetaDegrees)
|
||||
|
||||
tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
||||
trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
||||
brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
||||
blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
|
||||
tlx, tly := Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
||||
trx, try := Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
||||
brx, bry := Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
||||
blx, bly := Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
|
||||
|
||||
return BoxCorners{
|
||||
TopLeft: Point{tlx, tly},
|
||||
|
|
188
box_test.go
Normal file
|
@ -0,0 +1,188 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestBoxClone(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
|
||||
b := a.Clone()
|
||||
assert.True(a.Equals(b))
|
||||
assert.True(b.Equals(a))
|
||||
}
|
||||
|
||||
func TestBoxEquals(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
|
||||
b := Box{Top: 10, Left: 10, Right: 30, Bottom: 30}
|
||||
c := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
|
||||
assert.True(a.Equals(a))
|
||||
assert.True(a.Equals(c))
|
||||
assert.True(c.Equals(a))
|
||||
assert.False(a.Equals(b))
|
||||
assert.False(c.Equals(b))
|
||||
assert.False(b.Equals(a))
|
||||
assert.False(b.Equals(c))
|
||||
}
|
||||
|
||||
func TestBoxIsBiggerThan(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
|
||||
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
|
||||
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
|
||||
assert.True(a.IsBiggerThan(b))
|
||||
assert.False(a.IsBiggerThan(c))
|
||||
assert.True(c.IsBiggerThan(a))
|
||||
}
|
||||
|
||||
func TestBoxIsSmallerThan(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
|
||||
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
|
||||
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
|
||||
assert.False(a.IsSmallerThan(b))
|
||||
assert.True(a.IsSmallerThan(c))
|
||||
assert.False(c.IsSmallerThan(a))
|
||||
}
|
||||
|
||||
func TestBoxGrow(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 1, Left: 2, Right: 15, Bottom: 15}
|
||||
b := Box{Top: 4, Left: 5, Right: 30, Bottom: 35}
|
||||
c := a.Grow(b)
|
||||
assert.False(c.Equals(b))
|
||||
assert.False(c.Equals(a))
|
||||
assert.Equal(1, c.Top)
|
||||
assert.Equal(2, c.Left)
|
||||
assert.Equal(30, c.Right)
|
||||
assert.Equal(35, c.Bottom)
|
||||
}
|
||||
|
||||
func TestBoxFit(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
|
||||
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
|
||||
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
|
||||
|
||||
fab := a.Fit(b)
|
||||
assert.Equal(a.Left, fab.Left)
|
||||
assert.Equal(a.Right, fab.Right)
|
||||
assert.True(fab.Top < fab.Bottom)
|
||||
assert.True(fab.Left < fab.Right)
|
||||
assert.True(math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
|
||||
|
||||
fac := a.Fit(c)
|
||||
assert.Equal(a.Top, fac.Top)
|
||||
assert.Equal(a.Bottom, fac.Bottom)
|
||||
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
|
||||
}
|
||||
|
||||
func TestBoxConstrain(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
|
||||
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
|
||||
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
|
||||
|
||||
cab := a.Constrain(b)
|
||||
assert.Equal(64, cab.Top)
|
||||
assert.Equal(64, cab.Left)
|
||||
assert.Equal(192, cab.Right)
|
||||
assert.Equal(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)
|
||||
}
|
||||
|
||||
func TestBoxOuterConstrain(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
box := NewBox(0, 0, 100, 100)
|
||||
canvas := NewBox(5, 5, 95, 95)
|
||||
taller := NewBox(-10, 5, 50, 50)
|
||||
|
||||
c := canvas.OuterConstrain(box, taller)
|
||||
assert.Equal(15, c.Top, c.String())
|
||||
assert.Equal(5, c.Left, c.String())
|
||||
assert.Equal(95, c.Right, c.String())
|
||||
assert.Equal(95, c.Bottom, c.String())
|
||||
|
||||
wider := NewBox(5, 5, 110, 50)
|
||||
d := canvas.OuterConstrain(box, wider)
|
||||
assert.Equal(5, d.Top, d.String())
|
||||
assert.Equal(5, d.Left, d.String())
|
||||
assert.Equal(85, d.Right, d.String())
|
||||
assert.Equal(95, d.Bottom, d.String())
|
||||
}
|
||||
|
||||
func TestBoxShift(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
b := Box{
|
||||
Top: 5,
|
||||
Left: 5,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestBoxCenter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
b := Box{
|
||||
Top: 10,
|
||||
Left: 10,
|
||||
Right: 20,
|
||||
Bottom: 30,
|
||||
}
|
||||
cx, cy := b.Center()
|
||||
assert.Equal(15, cx)
|
||||
assert.Equal(20, cy)
|
||||
}
|
||||
|
||||
func TestBoxCornersCenter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BoxCorners{
|
||||
TopLeft: Point{5, 5},
|
||||
TopRight: Point{15, 5},
|
||||
BottomRight: Point{15, 15},
|
||||
BottomLeft: Point{5, 15},
|
||||
}
|
||||
|
||||
cx, cy := bc.Center()
|
||||
assert.Equal(10, cx)
|
||||
assert.Equal(10, cy)
|
||||
}
|
||||
|
||||
func TestBoxCornersRotate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := BoxCorners{
|
||||
TopLeft: Point{5, 5},
|
||||
TopRight: Point{15, 5},
|
||||
BottomRight: Point{15, 15},
|
||||
BottomLeft: Point{5, 15},
|
||||
}
|
||||
|
||||
rotated := bc.Rotate(45)
|
||||
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
||||
}
|
74
chart.go
|
@ -6,8 +6,7 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// Chart is what we're drawing.
|
||||
|
@ -15,8 +14,6 @@ type Chart struct {
|
|||
Title string
|
||||
TitleStyle Style
|
||||
|
||||
ColorPalette ColorPalette
|
||||
|
||||
Width int
|
||||
Height int
|
||||
DPI float64
|
||||
|
@ -101,11 +98,11 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
xr, yr, yra := c.getRanges()
|
||||
canvasBox := c.getDefaultCanvasBox()
|
||||
xf, yf, yfa := c.getValueFormatters()
|
||||
|
||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||
|
||||
err = c.checkRanges(xr, yr, yra)
|
||||
if err != nil {
|
||||
// (try to) dump the raw background to the stream.
|
||||
r.Save(w)
|
||||
return err
|
||||
}
|
||||
|
@ -178,10 +175,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
for _, s := range c.Series {
|
||||
if s.GetStyle().IsZero() || s.GetStyle().Show {
|
||||
seriesAxis := s.GetYAxis()
|
||||
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
|
||||
if bvp, isBoundedValueProvider := s.(BoundedValueProvider); isBoundedValueProvider {
|
||||
seriesLength := bvp.Len()
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy1, vy2 := bvp.GetBoundedValues(index)
|
||||
vx, vy1, vy2 := bvp.GetBoundedValue(index)
|
||||
|
||||
minx = math.Min(minx, vx)
|
||||
maxx = math.Max(maxx, vx)
|
||||
|
@ -199,10 +196,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
seriesMappedToSecondaryAxis = true
|
||||
}
|
||||
}
|
||||
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
} else if vp, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
seriesLength := vp.Len()
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy := vp.GetValues(index)
|
||||
vx, vy := vp.GetValue(index)
|
||||
|
||||
minx = math.Min(minx, vx)
|
||||
maxx = math.Max(maxx, vx)
|
||||
|
@ -263,15 +260,11 @@ 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 {
|
||||
delta := yrange.GetDelta()
|
||||
roundTo := util.Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := util.Math.RoundDown(yrange.GetMin(), roundTo), util.Math.RoundUp(yrange.GetMax(), roundTo)
|
||||
|
||||
yrange.SetMin(rmin)
|
||||
yrange.SetMax(rmax)
|
||||
}
|
||||
delta := yrange.GetDelta()
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
|
||||
yrange.SetMin(rmin)
|
||||
yrange.SetMax(rmax)
|
||||
}
|
||||
|
||||
if len(c.YAxisSecondary.Ticks) > 0 {
|
||||
|
@ -286,13 +279,11 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
yrangeAlt.SetMin(minya)
|
||||
yrangeAlt.SetMax(maxya)
|
||||
|
||||
if c.YAxisSecondary.Style.Show {
|
||||
delta := yrangeAlt.GetDelta()
|
||||
roundTo := util.Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := util.Math.RoundDown(yrangeAlt.GetMin(), roundTo), util.Math.RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
yrangeAlt.SetMin(rmin)
|
||||
yrangeAlt.SetMax(rmax)
|
||||
}
|
||||
delta := yrangeAlt.GetDelta()
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
yrangeAlt.SetMin(rmin)
|
||||
yrangeAlt.SetMax(rmax)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -349,13 +340,13 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
|
|||
}
|
||||
}
|
||||
if c.XAxis.ValueFormatter != nil {
|
||||
x = c.XAxis.GetValueFormatter()
|
||||
x = c.XAxis.ValueFormatter
|
||||
}
|
||||
if c.YAxis.ValueFormatter != nil {
|
||||
y = c.YAxis.GetValueFormatter()
|
||||
y = c.YAxis.ValueFormatter
|
||||
}
|
||||
if c.YAxisSecondary.ValueFormatter != nil {
|
||||
ya = c.YAxisSecondary.GetValueFormatter()
|
||||
ya = c.YAxisSecondary.ValueFormatter
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -487,7 +478,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 {
|
||||
r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
|
||||
r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
|
||||
r.SetFontColor(c.TitleStyle.GetFontColor(DefaultTextColor))
|
||||
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||||
r.SetFontSize(titleFontSize)
|
||||
|
||||
|
@ -505,24 +496,25 @@ func (c Chart) drawTitle(r Renderer) {
|
|||
|
||||
func (c Chart) styleDefaultsBackground() Style {
|
||||
return Style{
|
||||
FillColor: c.GetColorPalette().BackgroundColor(),
|
||||
StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
|
||||
FillColor: DefaultBackgroundColor,
|
||||
StrokeColor: DefaultBackgroundStrokeColor,
|
||||
StrokeWidth: DefaultBackgroundStrokeWidth,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Chart) styleDefaultsCanvas() Style {
|
||||
return Style{
|
||||
FillColor: c.GetColorPalette().CanvasColor(),
|
||||
StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
|
||||
FillColor: DefaultCanvasColor,
|
||||
StrokeColor: DefaultCanvasStrokeColor,
|
||||
StrokeWidth: DefaultCanvasStrokeWidth,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||
strokeColor := GetDefaultColor(seriesIndex)
|
||||
return Style{
|
||||
DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||
StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||
DotColor: strokeColor,
|
||||
StrokeColor: strokeColor,
|
||||
StrokeWidth: DefaultSeriesLineWidth,
|
||||
Font: c.GetFont(),
|
||||
FontSize: DefaultFontSize,
|
||||
|
@ -532,9 +524,9 @@ func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
|||
func (c Chart) styleDefaultsAxes() Style {
|
||||
return Style{
|
||||
Font: c.GetFont(),
|
||||
FontColor: c.GetColorPalette().TextColor(),
|
||||
FontColor: DefaultAxisColor,
|
||||
FontSize: DefaultAxisFontSize,
|
||||
StrokeColor: c.GetColorPalette().AxisStrokeColor(),
|
||||
StrokeColor: DefaultAxisColor,
|
||||
StrokeWidth: DefaultAxisLineWidth,
|
||||
}
|
||||
}
|
||||
|
@ -545,14 +537,6 @@ func (c Chart) styleDefaultsElements() Style {
|
|||
}
|
||||
}
|
||||
|
||||
// GetColorPalette returns the color palette for the chart.
|
||||
func (c Chart) GetColorPalette() ColorPalette {
|
||||
if c.ColorPalette != nil {
|
||||
return c.ColorPalette
|
||||
}
|
||||
return DefaultColorPalette
|
||||
}
|
||||
|
||||
// Box returns the chart bounds as a box.
|
||||
func (c Chart) Box() Box {
|
||||
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||
|
|
575
chart_test.go
Normal file
|
@ -0,0 +1,575 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func TestChartGetDPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultDPI, unset.GetDPI())
|
||||
assert.Equal(192, unset.GetDPI(192))
|
||||
|
||||
set := Chart{DPI: 128}
|
||||
assert.Equal(128, set.GetDPI())
|
||||
assert.Equal(128, set.GetDPI(192))
|
||||
}
|
||||
|
||||
func TestChartGetFont(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
|
||||
unset := Chart{}
|
||||
assert.Nil(unset.GetFont())
|
||||
|
||||
set := Chart{Font: f}
|
||||
assert.NotNil(set.GetFont())
|
||||
}
|
||||
|
||||
func TestChartGetWidth(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultChartWidth, unset.GetWidth())
|
||||
|
||||
set := Chart{Width: DefaultChartWidth + 10}
|
||||
assert.Equal(DefaultChartWidth+10, set.GetWidth())
|
||||
}
|
||||
|
||||
func TestChartGetHeight(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
unset := Chart{}
|
||||
assert.Equal(DefaultChartHeight, unset.GetHeight())
|
||||
|
||||
set := Chart{Height: DefaultChartHeight + 10}
|
||||
assert.Equal(DefaultChartHeight+10, set.GetHeight())
|
||||
}
|
||||
|
||||
func TestChartGetRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||
},
|
||||
ContinuousSeries{
|
||||
YAxis: YAxisSecondary,
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xrange, yrange, yrangeAlt := c.getRanges()
|
||||
assert.Equal(-2.0, xrange.GetMin())
|
||||
assert.Equal(5.0, xrange.GetMax())
|
||||
|
||||
assert.Equal(-2.1, yrange.GetMin())
|
||||
assert.Equal(4.5, yrange.GetMax())
|
||||
|
||||
assert.Equal(10.0, yrangeAlt.GetMin())
|
||||
assert.Equal(14.0, yrangeAlt.GetMax())
|
||||
|
||||
cSet := Chart{
|
||||
XAxis: XAxis{
|
||||
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||
},
|
||||
ContinuousSeries{
|
||||
YAxis: YAxisSecondary,
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xr2, yr2, yra2 := cSet.getRanges()
|
||||
assert.Equal(9.8, xr2.GetMin())
|
||||
assert.Equal(19.8, xr2.GetMax())
|
||||
|
||||
assert.Equal(9.9, yr2.GetMin())
|
||||
assert.Equal(19.9, yr2.GetMax())
|
||||
|
||||
assert.Equal(9.7, yra2.GetMin())
|
||||
assert.Equal(19.7, yra2.GetMax())
|
||||
}
|
||||
|
||||
func TestChartGetRangesUseTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// this test asserts that ticks should supercede manual ranges when generating the overall ranges.
|
||||
|
||||
c := Chart{
|
||||
YAxis: YAxis{
|
||||
Ticks: []Tick{
|
||||
{0.0, "Zero"},
|
||||
{1.0, "1.0"},
|
||||
{2.0, "2.0"},
|
||||
{3.0, "3.0"},
|
||||
{4.0, "4.0"},
|
||||
{5.0, "Five"},
|
||||
},
|
||||
Range: &ContinuousRange{
|
||||
Min: -5.0,
|
||||
Max: 5.0,
|
||||
},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xr, yr, yar := c.getRanges()
|
||||
assert.Equal(-2.0, xr.GetMin())
|
||||
assert.Equal(2.0, xr.GetMax())
|
||||
assert.Equal(0.0, yr.GetMin())
|
||||
assert.Equal(5.0, yr.GetMax())
|
||||
assert.True(yar.IsZero(), yar.String())
|
||||
}
|
||||
|
||||
func TestChartGetRangesUseUserRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
YAxis: YAxis{
|
||||
Range: &ContinuousRange{
|
||||
Min: -5.0,
|
||||
Max: 5.0,
|
||||
},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xr, yr, yar := c.getRanges()
|
||||
assert.Equal(-2.0, xr.GetMin())
|
||||
assert.Equal(2.0, xr.GetMax())
|
||||
assert.Equal(-5.0, yr.GetMin())
|
||||
assert.Equal(5.0, yr.GetMax())
|
||||
assert.True(yar.IsZero(), yar.String())
|
||||
}
|
||||
|
||||
func TestChartGetBackgroundStyle(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Background: Style{
|
||||
FillColor: drawing.ColorBlack,
|
||||
},
|
||||
}
|
||||
|
||||
bs := c.getBackgroundStyle()
|
||||
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
}
|
||||
|
||||
func TestChartGetCanvasStyle(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Canvas: Style{
|
||||
FillColor: drawing.ColorBlack,
|
||||
},
|
||||
}
|
||||
|
||||
bs := c.getCanvasStyle()
|
||||
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String())
|
||||
}
|
||||
|
||||
func TestChartGetDefaultCanvasBox(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
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)
|
||||
|
||||
custom := Chart{
|
||||
Background: Style{
|
||||
Padding: Box{
|
||||
Top: DefaultBackgroundPadding.Top + 1,
|
||||
Left: DefaultBackgroundPadding.Left + 1,
|
||||
Right: DefaultBackgroundPadding.Right + 1,
|
||||
Bottom: DefaultBackgroundPadding.Bottom + 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
canvasBoxCustom := custom.getDefaultCanvasBox()
|
||||
assert.False(canvasBoxCustom.IsZero())
|
||||
assert.Equal(DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top)
|
||||
assert.Equal(DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left)
|
||||
assert.Equal(c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right)
|
||||
assert.Equal(c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom)
|
||||
}
|
||||
|
||||
func TestChartGetValueFormatters(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{-2.1, -1.0, 0, 1.0, 2.0},
|
||||
},
|
||||
ContinuousSeries{
|
||||
YAxis: YAxisSecondary,
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{10.0, 11.0, 12.0, 13.0, 14.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dxf, dyf, dyaf := c.getValueFormatters()
|
||||
assert.NotNil(dxf)
|
||||
assert.NotNil(dyf)
|
||||
assert.NotNil(dyaf)
|
||||
}
|
||||
|
||||
func TestChartHasAxes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.False(Chart{}.hasAxes())
|
||||
|
||||
x := Chart{
|
||||
XAxis: XAxis{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(x.hasAxes())
|
||||
|
||||
y := Chart{
|
||||
YAxis: YAxis{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(y.hasAxes())
|
||||
|
||||
ya := Chart{
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(ya.hasAxes())
|
||||
}
|
||||
|
||||
func TestChartGetAxesTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
r, err := PNG(1024, 1024)
|
||||
assert.Nil(err)
|
||||
|
||||
c := Chart{
|
||||
XAxis: XAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||
},
|
||||
YAxisSecondary: YAxis{
|
||||
Style: Style{Show: true},
|
||||
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||
},
|
||||
}
|
||||
xr, yr, yar := c.getRanges()
|
||||
|
||||
xt, yt, yat := c.getAxesTicks(r, xr, yr, yar, FloatValueFormatter, FloatValueFormatter, FloatValueFormatter)
|
||||
assert.NotEmpty(xt)
|
||||
assert.NotEmpty(yt)
|
||||
assert.NotEmpty(yat)
|
||||
}
|
||||
|
||||
func TestChartSingleSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
now := time.Now()
|
||||
c := Chart{
|
||||
Title: "Hello!",
|
||||
TitleStyle: StyleShow(),
|
||||
Width: 1024,
|
||||
Height: 400,
|
||||
YAxis: YAxis{
|
||||
Style: StyleShow(),
|
||||
Range: &ContinuousRange{
|
||||
Min: 0.0,
|
||||
Max: 4.0,
|
||||
},
|
||||
},
|
||||
XAxis: XAxis{
|
||||
Style: StyleShow(),
|
||||
},
|
||||
Series: []Series{
|
||||
TimeSeries{
|
||||
Name: "goog",
|
||||
XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)},
|
||||
YValues: []float64{1.0, 2.0, 3.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.NotEmpty(buffer.Bytes())
|
||||
}
|
||||
|
||||
func TestChartRegressionBadRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{math.Inf(1), math.Inf(1), math.Inf(1), math.Inf(1), math.Inf(1)},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.True(true, "Render needs to finish.")
|
||||
}
|
||||
|
||||
func TestChartRegressionBadRangesByUser(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
YAxis: YAxis{
|
||||
Range: &ContinuousRange{
|
||||
Min: math.Inf(-1),
|
||||
Max: math.Inf(1), // this could really happen? eh.
|
||||
},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.True(true, "Render needs to finish.")
|
||||
}
|
||||
|
||||
func TestChartValidatesSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(c.validateSeries())
|
||||
|
||||
c = Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotNil(c.validateSeries())
|
||||
}
|
||||
|
||||
func TestChartCheckRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0},
|
||||
YValues: []float64{3.10, 3.14},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func TestChartCheckRangesWithRanges(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
XAxis: XAxis{
|
||||
Range: &ContinuousRange{
|
||||
Min: 0,
|
||||
Max: 10,
|
||||
},
|
||||
},
|
||||
YAxis: YAxis{
|
||||
Range: &ContinuousRange{
|
||||
Min: 0,
|
||||
Max: 5,
|
||||
},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: []float64{1.0, 2.0},
|
||||
YValues: []float64{3.14, 3.14},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
xr, yr, yra := c.getRanges()
|
||||
assert.Nil(c.checkRanges(xr, yr, yra))
|
||||
}
|
||||
|
||||
func at(i image.Image, x, y int) drawing.Color {
|
||||
return drawing.ColorFromAlphaMixedRGBA(i.At(x, y).RGBA())
|
||||
}
|
||||
|
||||
func TestChartE2ELine(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Height: 50,
|
||||
Width: 50,
|
||||
Canvas: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
},
|
||||
Background: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Sequence.Float64(0, 4, 1),
|
||||
YValues: Sequence.Float64(0, 4, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.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))
|
||||
}
|
||||
|
||||
func TestChartE2ELineWithFill(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
c := Chart{
|
||||
Height: 50,
|
||||
Width: 50,
|
||||
Canvas: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
},
|
||||
Background: Style{
|
||||
Padding: Box{IsSet: true},
|
||||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
Style: Style{
|
||||
Show: true,
|
||||
StrokeColor: drawing.ColorBlue,
|
||||
FillColor: drawing.ColorRed,
|
||||
},
|
||||
XValues: Sequence.Float64(0, 4, 1),
|
||||
YValues: Sequence.Float64(0, 4, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var 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))
|
||||
}
|
184
colors.go
|
@ -1,184 +0,0 @@
|
|||
package chart
|
||||
|
||||
import "git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
|
||||
var (
|
||||
// ColorWhite is white.
|
||||
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
||||
// ColorBlue is the basic theme blue color.
|
||||
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
|
||||
// ColorCyan is the basic theme cyan color.
|
||||
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
|
||||
// ColorGreen is the basic theme green color.
|
||||
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
|
||||
// ColorRed is the basic theme red color.
|
||||
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
|
||||
// ColorOrange is the basic theme orange color.
|
||||
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
|
||||
// ColorYellow is the basic theme yellow color.
|
||||
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
|
||||
// ColorBlack is the basic theme black color.
|
||||
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
||||
// ColorLightGray is the basic theme light gray color.
|
||||
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
||||
|
||||
// ColorAlternateBlue is a alternate theme color.
|
||||
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
|
||||
// ColorAlternateGreen is a alternate theme color.
|
||||
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
|
||||
// ColorAlternateGray is a alternate theme color.
|
||||
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
|
||||
// ColorAlternateYellow is a alternate theme color.
|
||||
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
|
||||
// ColorAlternateLightGray is a alternate theme color.
|
||||
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
|
||||
|
||||
// ColorTransparent is a transparent (alpha zero) color.
|
||||
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultBackgroundColor is the default chart background color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultBackgroundColor = ColorWhite
|
||||
// DefaultBackgroundStrokeColor is the default chart border color.
|
||||
// It is equivalent to color:white.
|
||||
DefaultBackgroundStrokeColor = ColorWhite
|
||||
// DefaultCanvasColor is the default chart canvas color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultCanvasColor = ColorWhite
|
||||
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultCanvasStrokeColor = ColorWhite
|
||||
// DefaultTextColor is the default chart text color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultTextColor = ColorBlack
|
||||
// DefaultAxisColor is the default chart axis line color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultAxisColor = ColorBlack
|
||||
// DefaultStrokeColor is the default chart border color.
|
||||
// It is equivalent to #efefef.
|
||||
DefaultStrokeColor = ColorLightGray
|
||||
// DefaultFillColor is the default fill color.
|
||||
// It is equivalent to #0074d9.
|
||||
DefaultFillColor = ColorBlue
|
||||
// DefaultAnnotationFillColor is the default annotation background color.
|
||||
DefaultAnnotationFillColor = ColorWhite
|
||||
// DefaultGridLineColor is the default grid line color.
|
||||
DefaultGridLineColor = ColorLightGray
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultColors are a couple default series colors.
|
||||
DefaultColors = []drawing.Color{
|
||||
ColorBlue,
|
||||
ColorGreen,
|
||||
ColorRed,
|
||||
ColorCyan,
|
||||
ColorOrange,
|
||||
}
|
||||
|
||||
// DefaultAlternateColors are a couple alternate colors.
|
||||
DefaultAlternateColors = []drawing.Color{
|
||||
ColorAlternateBlue,
|
||||
ColorAlternateGreen,
|
||||
ColorAlternateGray,
|
||||
ColorAlternateYellow,
|
||||
ColorBlue,
|
||||
ColorGreen,
|
||||
ColorRed,
|
||||
ColorCyan,
|
||||
ColorOrange,
|
||||
}
|
||||
)
|
||||
|
||||
// GetDefaultColor returns a color from the default list by index.
|
||||
// NOTE: the index will wrap around (using a modulo).
|
||||
func GetDefaultColor(index int) drawing.Color {
|
||||
finalIndex := index % len(DefaultColors)
|
||||
return DefaultColors[finalIndex]
|
||||
}
|
||||
|
||||
// GetAlternateColor returns a color from the default list by index.
|
||||
// NOTE: the index will wrap around (using a modulo).
|
||||
func GetAlternateColor(index int) drawing.Color {
|
||||
finalIndex := index % len(DefaultAlternateColors)
|
||||
return DefaultAlternateColors[finalIndex]
|
||||
}
|
||||
|
||||
// ColorPalette is a set of colors that.
|
||||
type ColorPalette interface {
|
||||
BackgroundColor() drawing.Color
|
||||
BackgroundStrokeColor() drawing.Color
|
||||
CanvasColor() drawing.Color
|
||||
CanvasStrokeColor() drawing.Color
|
||||
AxisStrokeColor() drawing.Color
|
||||
TextColor() drawing.Color
|
||||
GetSeriesColor(index int) drawing.Color
|
||||
}
|
||||
|
||||
// DefaultColorPalette represents the default palatte.
|
||||
var DefaultColorPalette defaultColorPalette
|
||||
|
||||
type defaultColorPalette struct{}
|
||||
|
||||
func (dp defaultColorPalette) BackgroundColor() drawing.Color {
|
||||
return DefaultBackgroundColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||
return DefaultBackgroundStrokeColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) CanvasColor() drawing.Color {
|
||||
return DefaultCanvasColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
|
||||
return DefaultCanvasStrokeColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
|
||||
return DefaultAxisColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) TextColor() drawing.Color {
|
||||
return DefaultTextColor
|
||||
}
|
||||
|
||||
func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||
return GetDefaultColor(index)
|
||||
}
|
||||
|
||||
// AlternateColorPalette represents the default palatte.
|
||||
var AlternateColorPalette alternateColorPalette
|
||||
|
||||
type alternateColorPalette struct{}
|
||||
|
||||
func (ap alternateColorPalette) BackgroundColor() drawing.Color {
|
||||
return DefaultBackgroundColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||
return DefaultBackgroundStrokeColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) CanvasColor() drawing.Color {
|
||||
return DefaultCanvasColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
|
||||
return DefaultCanvasStrokeColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
|
||||
return DefaultAxisColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) TextColor() drawing.Color {
|
||||
return DefaultTextColor
|
||||
}
|
||||
|
||||
func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||
return GetAlternateColor(index)
|
||||
}
|
|
@ -7,7 +7,7 @@ type ConcatSeries []Series
|
|||
func (cs ConcatSeries) Len() int {
|
||||
total := 0
|
||||
for _, s := range cs {
|
||||
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
total += typed.Len()
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,10 @@ func (cs ConcatSeries) Len() int {
|
|||
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
|
||||
cursor := 0
|
||||
for _, s := range cs {
|
||||
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||
len := typed.Len()
|
||||
if index < cursor+len {
|
||||
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
|
||||
x, y = typed.GetValue(index - cursor) //FENCEPOSTS.
|
||||
return
|
||||
}
|
||||
cursor += typed.Len()
|
||||
|
|
41
concat_series_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestConcatSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
|
||||
s2 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(11, 20.0),
|
||||
YValues: Sequence.Float64(10.0, 1.0),
|
||||
}
|
||||
|
||||
s3 := ContinuousSeries{
|
||||
XValues: Sequence.Float64(21, 30.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
|
||||
cs := ConcatSeries([]Series{s1, s2, s3})
|
||||
assert.Equal(30, cs.Len())
|
||||
|
||||
x0, y0 := cs.GetValue(0)
|
||||
assert.Equal(1.0, x0)
|
||||
assert.Equal(1.0, y0)
|
||||
|
||||
xm, ym := cs.GetValue(19)
|
||||
assert.Equal(20.0, xm)
|
||||
assert.Equal(1.0, ym)
|
||||
|
||||
xn, yn := cs.GetValue(29)
|
||||
assert.Equal(30.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
}
|
22
continuous_range_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestRangeTranslate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
|
||||
r := ContinuousRange{Domain: 1000}
|
||||
r.Min, r.Max = Math.MinAndMax(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))
|
||||
}
|
|
@ -2,13 +2,6 @@ package chart
|
|||
|
||||
import "fmt"
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*ContinuousSeries)(nil)
|
||||
_ FirstValuesProvider = (*ContinuousSeries)(nil)
|
||||
_ LastValuesProvider = (*ContinuousSeries)(nil)
|
||||
)
|
||||
|
||||
// ContinuousSeries represents a line on a chart.
|
||||
type ContinuousSeries struct {
|
||||
Name string
|
||||
|
@ -38,18 +31,13 @@ func (cs ContinuousSeries) Len() int {
|
|||
return len(cs.XValues)
|
||||
}
|
||||
|
||||
// GetValues gets the x,y values at a given index.
|
||||
func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
|
||||
// GetValue gets a value at a given index.
|
||||
func (cs ContinuousSeries) GetValue(index int) (float64, float64) {
|
||||
return cs.XValues[index], cs.YValues[index]
|
||||
}
|
||||
|
||||
// GetFirstValues gets the first x,y values.
|
||||
func (cs ContinuousSeries) GetFirstValues() (float64, float64) {
|
||||
return cs.XValues[0], cs.YValues[0]
|
||||
}
|
||||
|
||||
// GetLastValues gets the last x,y values.
|
||||
func (cs ContinuousSeries) GetLastValues() (float64, float64) {
|
||||
// GetLastValue gets the last value.
|
||||
func (cs ContinuousSeries) GetLastValue() (float64, float64) {
|
||||
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
|
||||
}
|
||||
|
||||
|
|
72
continuous_series_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestContinuousSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(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)
|
||||
|
||||
xn, yn := cs.GetValue(9)
|
||||
assert.Equal(10.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
|
||||
xn, yn = cs.GetLastValue()
|
||||
assert.Equal(10.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
}
|
||||
|
||||
func TestContinuousSeriesValueFormatter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cs := ContinuousSeries{
|
||||
XValueFormatter: func(v interface{}) string {
|
||||
return fmt.Sprintf("%f foo", v)
|
||||
},
|
||||
YValueFormatter: func(v interface{}) string {
|
||||
return fmt.Sprintf("%f bar", v)
|
||||
},
|
||||
}
|
||||
|
||||
xf, yf := cs.GetValueFormatters()
|
||||
assert.Equal("0.100000 foo", xf(0.1))
|
||||
assert.Equal("0.100000 bar", yf(0.1))
|
||||
}
|
||||
|
||||
func TestContinuousSeriesValidate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
assert.Nil(cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
}
|
426
date.go
Normal file
|
@ -0,0 +1,426 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllDaysMask is a bitmask of all the days of the week.
|
||||
AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
|
||||
// WeekDaysMask is a bitmask of all the weekdays of the week.
|
||||
WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
|
||||
//WeekendDaysMask is a bitmask of the weekend days of the week.
|
||||
WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
|
||||
)
|
||||
|
||||
var (
|
||||
// DaysOfWeek are all the time.Weekday in an array for utility purposes.
|
||||
DaysOfWeek = []time.Weekday{
|
||||
time.Sunday,
|
||||
time.Monday,
|
||||
time.Tuesday,
|
||||
time.Wednesday,
|
||||
time.Thursday,
|
||||
time.Friday,
|
||||
time.Saturday,
|
||||
}
|
||||
|
||||
// WeekDays are the business time.Weekday in an array.
|
||||
WeekDays = []time.Weekday{
|
||||
time.Monday,
|
||||
time.Tuesday,
|
||||
time.Wednesday,
|
||||
time.Thursday,
|
||||
time.Friday,
|
||||
}
|
||||
|
||||
// WeekendDays are the weekend time.Weekday in an array.
|
||||
WeekendDays = []time.Weekday{
|
||||
time.Sunday,
|
||||
time.Saturday,
|
||||
}
|
||||
|
||||
//Epoch is unix epoc saved for utility purposes.
|
||||
Epoch = time.Unix(0, 0)
|
||||
)
|
||||
|
||||
var (
|
||||
_easternLock sync.Mutex
|
||||
_eastern *time.Location
|
||||
)
|
||||
|
||||
// NYSEOpen is when the NYSE opens.
|
||||
func NYSEOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEClose is when the NYSE closes.
|
||||
func NYSEClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NASDAQOpen is when NASDAQ opens.
|
||||
func NASDAQOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NASDAQClose is when NASDAQ closes.
|
||||
func NASDAQClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEArcaOpen is when NYSEARCA opens.
|
||||
func NYSEArcaOpen() time.Time { return Date.Time(4, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// NYSEArcaClose is when NYSEARCA closes.
|
||||
func NYSEArcaClose() time.Time { return Date.Time(20, 0, 0, 0, Date.Eastern()) }
|
||||
|
||||
// HolidayProvider is a function that returns if a given time falls on a holiday.
|
||||
type HolidayProvider func(time.Time) bool
|
||||
|
||||
// defaultHolidayProvider implements `HolidayProvider` and just returns false.
|
||||
func defaultHolidayProvider(_ time.Time) bool { return false }
|
||||
|
||||
var (
|
||||
// Date contains utility functions that operate on dates.
|
||||
Date = &date{}
|
||||
)
|
||||
|
||||
type date struct{}
|
||||
|
||||
// IsNYSEHoliday returns if a date was/is on a nyse holiday day.
|
||||
func (d date) IsNYSEHoliday(t time.Time) bool {
|
||||
te := t.In(d.Eastern())
|
||||
if te.Year() == 2013 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 21
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 18
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 29
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 27
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 2
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 28
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2014 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 20
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 17
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 18
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 26
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 1
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 27
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2015 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 19
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 16
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 25
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 7
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 26
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2016 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 18
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 15
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 25
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 30
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 5
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 24 || te.Day() == 25
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 26
|
||||
}
|
||||
} else if te.Year() == 2017 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 16
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 20
|
||||
} else if te.Month() == 4 {
|
||||
return te.Day() == 15
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 29
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 23
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
} else if te.Year() == 2018 {
|
||||
if te.Month() == 1 {
|
||||
return te.Day() == 1 || te.Day() == 15
|
||||
} else if te.Month() == 2 {
|
||||
return te.Day() == 19
|
||||
} else if te.Month() == 3 {
|
||||
return te.Day() == 30
|
||||
} else if te.Month() == 5 {
|
||||
return te.Day() == 28
|
||||
} else if te.Month() == 7 {
|
||||
return te.Day() == 4
|
||||
} else if te.Month() == 9 {
|
||||
return te.Day() == 3
|
||||
} else if te.Month() == 11 {
|
||||
return te.Day() == 22
|
||||
} else if te.Month() == 12 {
|
||||
return te.Day() == 25
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNYSEArcaHoliday returns that returns if a given time falls on a holiday.
|
||||
func (d date) IsNYSEArcaHoliday(t time.Time) bool {
|
||||
return d.IsNYSEHoliday(t)
|
||||
}
|
||||
|
||||
// IsNASDAQHoliday returns if a date was a NASDAQ holiday day.
|
||||
func (d date) IsNASDAQHoliday(t time.Time) bool {
|
||||
return d.IsNYSEHoliday(t)
|
||||
}
|
||||
|
||||
// Time returns a new time.Time for the given clock components.
|
||||
func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time {
|
||||
return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
|
||||
}
|
||||
|
||||
func (d date) Date(year, month, day int, loc *time.Location) time.Time {
|
||||
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
// On returns the clock components of clock (hour,minute,second) on the date components of d.
|
||||
func (d date) On(clock, cd time.Time) time.Time {
|
||||
tzAdjusted := cd.In(clock.Location())
|
||||
return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
|
||||
}
|
||||
|
||||
// NoonOn is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date.
|
||||
func (d date) NoonOn(cd time.Time) time.Time {
|
||||
return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location())
|
||||
}
|
||||
|
||||
// Optional returns a pointer reference to a given time.
|
||||
func (d date) Optional(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// IsWeekDay returns if the day is a monday->friday.
|
||||
func (d date) IsWeekDay(day time.Weekday) bool {
|
||||
return !d.IsWeekendDay(day)
|
||||
}
|
||||
|
||||
// IsWeekendDay returns if the day is a monday->friday.
|
||||
func (d date) IsWeekendDay(day time.Weekday) bool {
|
||||
return day == time.Saturday || day == time.Sunday
|
||||
}
|
||||
|
||||
// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
|
||||
func (d date) Before(before, reference time.Time) bool {
|
||||
tzAdjustedBefore := before.In(reference.Location())
|
||||
if tzAdjustedBefore.Year() < reference.Year() {
|
||||
return true
|
||||
}
|
||||
if tzAdjustedBefore.Month() < reference.Month() {
|
||||
return true
|
||||
}
|
||||
return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day()
|
||||
}
|
||||
|
||||
// NextMarketOpen returns the next market open after a given time.
|
||||
func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||
afterLocalized := after.In(openTime.Location())
|
||||
todaysOpen := d.On(openTime, afterLocalized)
|
||||
|
||||
if isHoliday == nil {
|
||||
isHoliday = defaultHolidayProvider
|
||||
}
|
||||
|
||||
todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen)
|
||||
|
||||
if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay {
|
||||
return todaysOpen
|
||||
}
|
||||
|
||||
for cursorDay := 1; cursorDay < 7; cursorDay++ {
|
||||
newDay := todaysOpen.AddDate(0, 0, cursorDay)
|
||||
isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay)
|
||||
if isValidTradingDay {
|
||||
return d.On(openTime, newDay)
|
||||
}
|
||||
}
|
||||
panic("Have exhausted day window looking for next market open.")
|
||||
}
|
||||
|
||||
// NextMarketClose returns the next market close after a given time.
|
||||
func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||
afterLocalized := after.In(closeTime.Location())
|
||||
|
||||
if isHoliday == nil {
|
||||
isHoliday = defaultHolidayProvider
|
||||
}
|
||||
|
||||
todaysClose := d.On(closeTime, afterLocalized)
|
||||
if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
|
||||
return todaysClose
|
||||
}
|
||||
|
||||
if afterLocalized.Equal(todaysClose) { //rare but it might happen.
|
||||
return todaysClose
|
||||
}
|
||||
|
||||
for cursorDay := 1; cursorDay < 6; cursorDay++ {
|
||||
newDay := todaysClose.AddDate(0, 0, cursorDay)
|
||||
if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) {
|
||||
return d.On(closeTime, newDay)
|
||||
}
|
||||
}
|
||||
panic("Have exhausted day window looking for next market close.")
|
||||
}
|
||||
|
||||
// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates.
|
||||
func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) {
|
||||
startEastern := start.In(d.Eastern())
|
||||
endEastern := end.In(d.Eastern())
|
||||
|
||||
startMarketOpen := d.On(marketOpen, startEastern)
|
||||
startMarketClose := d.On(marketClose, startEastern)
|
||||
|
||||
if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) {
|
||||
if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) {
|
||||
if endEastern.Before(startMarketClose) {
|
||||
seconds += int64(endEastern.Sub(startEastern) / time.Second)
|
||||
} else {
|
||||
seconds += int64(startMarketClose.Sub(startEastern) / time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday)
|
||||
for d.Before(cursor, endEastern) {
|
||||
if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) {
|
||||
close := d.NextMarketClose(cursor, marketClose, isHoliday)
|
||||
seconds += int64(close.Sub(cursor) / time.Second)
|
||||
}
|
||||
cursor = cursor.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday)
|
||||
finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday)
|
||||
if endEastern.After(finalMarketOpen) {
|
||||
if endEastern.Before(finalMarketClose) {
|
||||
seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second)
|
||||
} else {
|
||||
seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
_secondsPerHour = 60 * 60
|
||||
_secondsPerDay = 60 * 60 * 24
|
||||
)
|
||||
|
||||
func (d date) DiffDays(t1, t2 time.Time) (days int) {
|
||||
t1n := t1.Unix()
|
||||
t2n := t2.Unix()
|
||||
diff := t2n - t1n //yields seconds
|
||||
return int(diff / (_secondsPerDay))
|
||||
}
|
||||
|
||||
func (d date) DiffHours(t1, t2 time.Time) (hours int) {
|
||||
t1n := t1.Unix()
|
||||
t2n := t2.Unix()
|
||||
diff := t2n - t1n //yields seconds
|
||||
return int(diff / (_secondsPerHour))
|
||||
}
|
||||
|
||||
// NextDay returns the timestamp advanced a day.
|
||||
func (d date) NextDay(ts time.Time) time.Time {
|
||||
return ts.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
// NextHour returns the next timestamp on the hour.
|
||||
func (d date) NextHour(ts time.Time) time.Time {
|
||||
//advance a full hour ...
|
||||
advanced := ts.Add(time.Hour)
|
||||
minutes := time.Duration(advanced.Minute()) * time.Minute
|
||||
final := advanced.Add(-minutes)
|
||||
return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location())
|
||||
}
|
||||
|
||||
// NextDayOfWeek returns the next instance of a given weekday after a given timestamp.
|
||||
func (d date) NextDayOfWeek(after time.Time, dayOfWeek time.Weekday) time.Time {
|
||||
afterWeekday := after.Weekday()
|
||||
if afterWeekday == dayOfWeek {
|
||||
return after.AddDate(0, 0, 7)
|
||||
}
|
||||
|
||||
// 1 vs 5 ~ add 4 days
|
||||
if afterWeekday < dayOfWeek {
|
||||
dayDelta := int(dayOfWeek - afterWeekday)
|
||||
return after.AddDate(0, 0, dayDelta)
|
||||
}
|
||||
|
||||
// 5 vs 1, add 7-(5-1) ~ 3 days
|
||||
dayDelta := 7 - int(afterWeekday-dayOfWeek)
|
||||
return after.AddDate(0, 0, dayDelta)
|
||||
}
|
||||
|
||||
// Start returns the earliest (min) time in a list of times.
|
||||
func (d date) Start(times []time.Time) time.Time {
|
||||
if len(times) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
start := times[0]
|
||||
for _, t := range times[1:] {
|
||||
if t.Before(start) {
|
||||
start = t
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// Start returns the earliest (min) time in a list of times.
|
||||
func (d date) End(times []time.Time) time.Time {
|
||||
if len(times) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
end := times[0]
|
||||
for _, t := range times[1:] {
|
||||
if t.After(end) {
|
||||
end = t
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
17
date_posix.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
// +build !windows
|
||||
|
||||
package chart
|
||||
|
||||
import "time"
|
||||
|
||||
// Eastern returns the eastern timezone.
|
||||
func (d date) Eastern() *time.Location {
|
||||
if _eastern == nil {
|
||||
_easternLock.Lock()
|
||||
defer _easternLock.Unlock()
|
||||
if _eastern == nil {
|
||||
_eastern, _ = time.LoadLocation("America/New_York")
|
||||
}
|
||||
}
|
||||
return _eastern
|
||||
}
|
288
date_test.go
Normal file
|
@ -0,0 +1,288 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func parse(v string) time.Time {
|
||||
ts, _ := time.Parse("2006-01-02", v)
|
||||
return ts
|
||||
}
|
||||
|
||||
func TestDateTime(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.Time(5, 6, 7, 8, time.UTC)
|
||||
assert.Equal(05, ts.Hour())
|
||||
assert.Equal(06, ts.Minute())
|
||||
assert.Equal(07, ts.Second())
|
||||
assert.Equal(8, ts.Nanosecond())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateDate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.Date(2015, 5, 6, time.UTC)
|
||||
assert.Equal(2015, ts.Year())
|
||||
assert.Equal(5, ts.Month())
|
||||
assert.Equal(6, ts.Day())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateOn(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ts := Date.On(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.Eastern()))
|
||||
assert.Equal(2016, ts.Year())
|
||||
assert.Equal(6, ts.Month())
|
||||
assert.Equal(7, ts.Day())
|
||||
assert.Equal(5, ts.Hour())
|
||||
assert.Equal(4, ts.Minute())
|
||||
assert.Equal(3, ts.Second())
|
||||
assert.Equal(2, ts.Nanosecond())
|
||||
assert.Equal(time.UTC, ts.Location())
|
||||
}
|
||||
|
||||
func TestDateNoonOn(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
noon := Date.NoonOn(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC))
|
||||
|
||||
assert.Equal(2016, noon.Year())
|
||||
assert.Equal(4, noon.Month())
|
||||
assert.Equal(3, noon.Day())
|
||||
assert.Equal(12, noon.Hour())
|
||||
assert.Equal(0, noon.Minute())
|
||||
assert.Equal(time.UTC, noon.Location())
|
||||
}
|
||||
|
||||
func TestDateBefore(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.True(Date.Before(parse("2015-07-02"), parse("2016-07-01")))
|
||||
assert.True(Date.Before(parse("2016-06-01"), parse("2016-07-01")))
|
||||
assert.True(Date.Before(parse("2016-07-01"), parse("2016-07-02")))
|
||||
|
||||
assert.False(Date.Before(parse("2016-07-01"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2016-07-03"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2016-08-03"), parse("2016-07-01")))
|
||||
assert.False(Date.Before(parse("2017-08-03"), parse("2016-07-01")))
|
||||
}
|
||||
|
||||
func TestDateBeforeHandlesTimezones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tuesdayUTC := time.Date(2016, 8, 02, 22, 00, 0, 0, time.UTC)
|
||||
mondayUTC := time.Date(2016, 8, 01, 1, 00, 0, 0, time.UTC)
|
||||
sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(Date.Before(sundayEST, tuesdayUTC))
|
||||
assert.False(Date.Before(sundayEST, mondayUTC))
|
||||
}
|
||||
|
||||
func TestNextMarketOpen(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Date.Eastern())
|
||||
todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Date.Eastern())
|
||||
tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Date.Eastern())
|
||||
mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(todayOpen.Equal(Date.NextMarketOpen(beforeOpen, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(tomorrowOpen.Equal(Date.NextMarketOpen(afterOpen, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
|
||||
assert.Equal(Date.Eastern(), todayOpen.Location())
|
||||
assert.Equal(Date.Eastern(), tomorrowOpen.Location())
|
||||
assert.Equal(Date.Eastern(), mondayOpen.Location())
|
||||
|
||||
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern())
|
||||
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(shouldbe.Equal(Date.NextMarketOpen(testRegression, NYSEOpen(), Date.IsNYSEHoliday)))
|
||||
}
|
||||
|
||||
func TestNextMarketClose(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Date.Eastern())
|
||||
todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Date.Eastern())
|
||||
tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Date.Eastern())
|
||||
mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Date.Eastern())
|
||||
|
||||
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern())
|
||||
|
||||
assert.True(todayClose.Equal(Date.NextMarketClose(beforeClose, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose(), Date.IsNYSEHoliday)))
|
||||
|
||||
assert.Equal(Date.Eastern(), todayClose.Location())
|
||||
assert.Equal(Date.Eastern(), tomorrowClose.Location())
|
||||
assert.Equal(Date.Eastern(), mondayClose.Location())
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetween(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 5 * 6.5 * 60 * 60
|
||||
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetween1D(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2016, 07, 22, 9, 45, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 22, 15, 45, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 6 * 60 * 60
|
||||
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestCalculateMarketSecondsBetweenLTM(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
end := time.Date(2016, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
|
||||
shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year.
|
||||
assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday))
|
||||
}
|
||||
|
||||
func TestDateNextHour(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern())
|
||||
next := Date.NextHour(start)
|
||||
assert.Equal(2015, next.Year())
|
||||
assert.Equal(07, next.Month())
|
||||
assert.Equal(01, next.Day())
|
||||
assert.Equal(10, next.Hour())
|
||||
assert.Equal(00, next.Minute())
|
||||
|
||||
next = Date.NextHour(next)
|
||||
assert.Equal(11, next.Hour())
|
||||
|
||||
next = Date.NextHour(next)
|
||||
assert.Equal(12, next.Hour())
|
||||
|
||||
}
|
||||
|
||||
func TestDateNextDayOfWeek(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
weds := Date.Date(2016, 8, 10, time.UTC)
|
||||
fri := Date.Date(2016, 8, 12, time.UTC)
|
||||
sun := Date.Date(2016, 8, 14, time.UTC)
|
||||
mon := Date.Date(2016, 8, 15, time.UTC)
|
||||
weds2 := Date.Date(2016, 8, 17, time.UTC)
|
||||
|
||||
nextFri := Date.NextDayOfWeek(weds, time.Friday)
|
||||
nextSunday := Date.NextDayOfWeek(weds, time.Sunday)
|
||||
nextMonday := Date.NextDayOfWeek(weds, time.Monday)
|
||||
nextWeds := Date.NextDayOfWeek(weds, time.Wednesday)
|
||||
|
||||
assert.Equal(fri.Year(), nextFri.Year())
|
||||
assert.Equal(fri.Month(), nextFri.Month())
|
||||
assert.Equal(fri.Day(), nextFri.Day())
|
||||
|
||||
assert.Equal(sun.Year(), nextSunday.Year())
|
||||
assert.Equal(sun.Month(), nextSunday.Month())
|
||||
assert.Equal(sun.Day(), nextSunday.Day())
|
||||
|
||||
assert.Equal(mon.Year(), nextMonday.Year())
|
||||
assert.Equal(mon.Month(), nextMonday.Month())
|
||||
assert.Equal(mon.Day(), nextMonday.Day())
|
||||
|
||||
assert.Equal(weds2.Year(), nextWeds.Year())
|
||||
assert.Equal(weds2.Month(), nextWeds.Month())
|
||||
assert.Equal(weds2.Day(), nextWeds.Day())
|
||||
|
||||
assert.Equal(time.UTC, nextFri.Location())
|
||||
assert.Equal(time.UTC, nextSunday.Location())
|
||||
assert.Equal(time.UTC, nextMonday.Location())
|
||||
}
|
||||
|
||||
func TestDateIsNYSEHoliday(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cursor := time.Date(2013, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
var holidays int
|
||||
for Date.Before(cursor, end) {
|
||||
if Date.IsNYSEHoliday(cursor) {
|
||||
holidays++
|
||||
}
|
||||
cursor = cursor.AddDate(0, 0, 1)
|
||||
}
|
||||
assert.Equal(holidays, 55)
|
||||
}
|
||||
|
||||
func TestTimeStart(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
times := []time.Time{
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
time.Now().AddDate(0, 0, -2),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -5),
|
||||
}
|
||||
|
||||
assert.InTimeDelta(Date.Start(times), times[4], time.Millisecond)
|
||||
}
|
||||
|
||||
func TestTimeEnd(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
times := []time.Time{
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
time.Now().AddDate(0, 0, -2),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -5),
|
||||
}
|
||||
|
||||
assert.InTimeDelta(Date.End(times), times[2], time.Millisecond)
|
||||
}
|
||||
|
||||
func TestDateDiffDays(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2017, 01, 10, 3, 0, 0, 0, time.UTC)
|
||||
t3 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
|
||||
|
||||
assert.Equal(48, Date.DiffDays(t2, t1))
|
||||
assert.Equal(2, Date.DiffDays(t3, t1)) // technically we should round down.
|
||||
}
|
||||
|
||||
func TestDateDiffHours(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC)
|
||||
t3 := time.Date(2017, 02, 28, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
assert.Equal(68, Date.DiffHours(t2, t1))
|
||||
assert.Equal(24, Date.DiffHours(t1, t3))
|
||||
}
|
17
date_windows.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
// +build windows
|
||||
|
||||
package chart
|
||||
|
||||
import "time"
|
||||
|
||||
// Eastern returns the eastern timezone.
|
||||
func (d date) Eastern() *time.Location {
|
||||
if _eastern == nil {
|
||||
_easternLock.Lock()
|
||||
defer _easternLock.Unlock()
|
||||
if _eastern == nil {
|
||||
_eastern, _ = time.LoadLocation("EST")
|
||||
}
|
||||
}
|
||||
return _eastern
|
||||
}
|
142
defaults.go
|
@ -1,5 +1,12 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultChartHeight is the default chart height.
|
||||
DefaultChartHeight = 400
|
||||
|
@ -75,6 +82,96 @@ const (
|
|||
DefaultBarWidth = 50
|
||||
)
|
||||
|
||||
var (
|
||||
// ColorWhite is white.
|
||||
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
||||
// ColorBlue is the basic theme blue color.
|
||||
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
|
||||
// ColorCyan is the basic theme cyan color.
|
||||
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
|
||||
// ColorGreen is the basic theme green color.
|
||||
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
|
||||
// ColorRed is the basic theme red color.
|
||||
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
|
||||
// ColorOrange is the basic theme orange color.
|
||||
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
|
||||
// ColorYellow is the basic theme yellow color.
|
||||
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
|
||||
// ColorBlack is the basic theme black color.
|
||||
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
||||
// ColorLightGray is the basic theme light gray color.
|
||||
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
||||
|
||||
// ColorAlternateBlue is a alternate theme color.
|
||||
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
|
||||
// ColorAlternateGreen is a alternate theme color.
|
||||
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
|
||||
// ColorAlternateGray is a alternate theme color.
|
||||
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
|
||||
// ColorAlternateYellow is a alternate theme color.
|
||||
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
|
||||
// ColorAlternateLightGray is a alternate theme color.
|
||||
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
|
||||
|
||||
// ColorTransparent is a transparent (alpha zero) color.
|
||||
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultBackgroundColor is the default chart background color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultBackgroundColor = ColorWhite
|
||||
// DefaultBackgroundStrokeColor is the default chart border color.
|
||||
// It is equivalent to color:white.
|
||||
DefaultBackgroundStrokeColor = ColorWhite
|
||||
// DefaultCanvasColor is the default chart canvas color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultCanvasColor = ColorWhite
|
||||
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
||||
// It is equivalent to css color:white.
|
||||
DefaultCanvasStrokeColor = ColorWhite
|
||||
// DefaultTextColor is the default chart text color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultTextColor = ColorBlack
|
||||
// DefaultAxisColor is the default chart axis line color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultAxisColor = ColorBlack
|
||||
// DefaultStrokeColor is the default chart border color.
|
||||
// It is equivalent to #efefef.
|
||||
DefaultStrokeColor = ColorLightGray
|
||||
// DefaultFillColor is the default fill color.
|
||||
// It is equivalent to #0074d9.
|
||||
DefaultFillColor = ColorBlue
|
||||
// DefaultAnnotationFillColor is the default annotation background color.
|
||||
DefaultAnnotationFillColor = ColorWhite
|
||||
// DefaultGridLineColor is the default grid line color.
|
||||
DefaultGridLineColor = ColorLightGray
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultColors are a couple default series colors.
|
||||
DefaultColors = []drawing.Color{
|
||||
ColorBlue,
|
||||
ColorGreen,
|
||||
ColorRed,
|
||||
ColorCyan,
|
||||
ColorOrange,
|
||||
}
|
||||
|
||||
// DefaultAlternateColors are a couple alternate colors.
|
||||
DefaultAlternateColors = []drawing.Color{
|
||||
ColorAlternateBlue,
|
||||
ColorAlternateGreen,
|
||||
ColorAlternateGray,
|
||||
ColorAlternateYellow,
|
||||
ColorBlue,
|
||||
ColorGreen,
|
||||
ColorRed,
|
||||
ColorCyan,
|
||||
ColorOrange,
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DashArrayDots is a dash array that represents '....' style stroke dashes.
|
||||
DashArrayDots = []int{1, 1}
|
||||
|
@ -86,18 +183,49 @@ var (
|
|||
DashArrayDashesLarge = []int{10, 10}
|
||||
)
|
||||
|
||||
// NewColor returns a new color.
|
||||
func NewColor(r, g, b, a uint8) drawing.Color {
|
||||
return drawing.Color{R: r, G: g, B: b, A: a}
|
||||
}
|
||||
|
||||
// GetDefaultColor returns a color from the default list by index.
|
||||
// NOTE: the index will wrap around (using a modulo).
|
||||
func GetDefaultColor(index int) drawing.Color {
|
||||
finalIndex := index % len(DefaultColors)
|
||||
return DefaultColors[finalIndex]
|
||||
}
|
||||
|
||||
// GetAlternateColor returns a color from the default list by index.
|
||||
// NOTE: the index will wrap around (using a modulo).
|
||||
func GetAlternateColor(index int) drawing.Color {
|
||||
finalIndex := index % len(DefaultAlternateColors)
|
||||
return DefaultAlternateColors[finalIndex]
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultAnnotationPadding is the padding around an annotation.
|
||||
DefaultAnnotationPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||
|
||||
// DefaultBackgroundPadding is the default canvas padding config.
|
||||
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||
)
|
||||
|
||||
const (
|
||||
// ContentTypePNG is the png mime type.
|
||||
ContentTypePNG = "image/png"
|
||||
|
||||
// ContentTypeSVG is the svg mime type.
|
||||
ContentTypeSVG = "image/svg+xml"
|
||||
var (
|
||||
_defaultFontLock sync.Mutex
|
||||
_defaultFont *truetype.Font
|
||||
)
|
||||
|
||||
// GetDefaultFont returns the default font (Roboto-Medium).
|
||||
func GetDefaultFont() (*truetype.Font, error) {
|
||||
if _defaultFont == nil {
|
||||
_defaultFontLock.Lock()
|
||||
defer _defaultFontLock.Unlock()
|
||||
if _defaultFont == nil {
|
||||
font, err := truetype.Parse(roboto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_defaultFont = font
|
||||
}
|
||||
}
|
||||
return _defaultFont, nil
|
||||
}
|
||||
|
|
47
draw.go
|
@ -1,10 +1,6 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
import "math"
|
||||
|
||||
var (
|
||||
// Draw contains helpers for drawing common objects.
|
||||
|
@ -14,7 +10,7 @@ var (
|
|||
type draw struct{}
|
||||
|
||||
// LineSeries draws a line series with a renderer.
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -22,7 +18,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
||||
v0x, v0y := vs.GetValues(0)
|
||||
v0x, v0y := vs.GetValue(0)
|
||||
x0 := cl + xrange.Translate(v0x)
|
||||
y0 := cb - yrange.Translate(v0y)
|
||||
|
||||
|
@ -35,13 +31,13 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValues(i)
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
}
|
||||
r.LineTo(x, util.Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, util.Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x, Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, Math.MinInt(cb, cb-yv0))
|
||||
r.LineTo(x0, y0)
|
||||
r.Fill()
|
||||
}
|
||||
|
@ -51,7 +47,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValues(i)
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
|
@ -60,34 +56,23 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
}
|
||||
|
||||
if style.ShouldDrawDot() {
|
||||
defaultDotWidth := style.GetDotWidth()
|
||||
dotWidth := style.GetDotWidth()
|
||||
|
||||
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||
|
||||
for i := 0; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValues(i)
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
|
||||
dotWidth := defaultDotWidth
|
||||
if style.DotWidthProvider != nil {
|
||||
dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
|
||||
}
|
||||
|
||||
if style.DotColorProvider != nil {
|
||||
dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
|
||||
|
||||
r.SetFillColor(dotColor)
|
||||
r.SetStrokeColor(dotColor)
|
||||
}
|
||||
|
||||
r.Circle(dotWidth, x, y)
|
||||
r.FillStroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BoundedSeries draws a series that implements BoundedValuesProvider.
|
||||
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
|
||||
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
|
||||
drawOffsetIndex := 0
|
||||
if len(drawOffsetIndexes) > 0 {
|
||||
drawOffsetIndex = drawOffsetIndexes[0]
|
||||
|
@ -96,7 +81,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
||||
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
|
||||
v0x, v0y1, v0y2 := bbs.GetBoundedValue(0)
|
||||
x0 := cl + xrange.Translate(v0x)
|
||||
y0 := cb - yrange.Translate(v0y1)
|
||||
|
||||
|
@ -111,7 +96,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < bbs.Len(); i++ {
|
||||
vx, vy1, vy2 = bbs.GetBoundedValues(i)
|
||||
vx, vy1, vy2 = bbs.GetBoundedValue(i)
|
||||
|
||||
xvalues[i] = vx
|
||||
y2values[i] = vy2
|
||||
|
@ -137,7 +122,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty
|
|||
}
|
||||
|
||||
// HistogramSeries draws a value provider as boxes from 0.
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -154,7 +139,7 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
|||
|
||||
//foreach datapoint, draw a box.
|
||||
for index := 0; index < seriesLength; index++ {
|
||||
vx, vy := vs.GetValues(index)
|
||||
vx, vy := vs.GetValue(index)
|
||||
y0 := yrange.Translate(0)
|
||||
x := cl + xrange.Translate(vx)
|
||||
y := yrange.Translate(vy)
|
||||
|
|
|
@ -57,11 +57,6 @@ func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
|
|||
return c
|
||||
}
|
||||
|
||||
// ColorChannelFromFloat returns a normalized byte from a given float value.
|
||||
func ColorChannelFromFloat(v float64) uint8 {
|
||||
return uint8(v * 255)
|
||||
}
|
||||
|
||||
// Color is our internal color type because color.Color is bullshit.
|
||||
type Color struct {
|
||||
R, G, B, A uint8
|
||||
|
|
53
drawing/color_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"image/color"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestColorFromHex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
white := ColorFromHex("FFFFFF")
|
||||
assert.Equal(ColorWhite, white)
|
||||
|
||||
shortWhite := ColorFromHex("FFF")
|
||||
assert.Equal(ColorWhite, shortWhite)
|
||||
|
||||
black := ColorFromHex("000000")
|
||||
assert.Equal(ColorBlack, black)
|
||||
|
||||
shortBlack := ColorFromHex("000")
|
||||
assert.Equal(ColorBlack, shortBlack)
|
||||
|
||||
red := ColorFromHex("FF0000")
|
||||
assert.Equal(ColorRed, red)
|
||||
|
||||
shortRed := ColorFromHex("F00")
|
||||
assert.Equal(ColorRed, shortRed)
|
||||
|
||||
green := ColorFromHex("00FF00")
|
||||
assert.Equal(ColorGreen, green)
|
||||
|
||||
shortGreen := ColorFromHex("0F0")
|
||||
assert.Equal(ColorGreen, shortGreen)
|
||||
|
||||
blue := ColorFromHex("0000FF")
|
||||
assert.Equal(ColorBlue, blue)
|
||||
|
||||
shortBlue := ColorFromHex("00F")
|
||||
assert.Equal(ColorBlue, shortBlue)
|
||||
}
|
||||
|
||||
func TestColorFromAlphaMixedRGBA(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
|
||||
assert.True(black.Equals(ColorBlack), black.String())
|
||||
|
||||
white := ColorFromAlphaMixedRGBA(color.White.RGBA())
|
||||
assert.True(white.Equals(ColorWhite), white.String())
|
||||
}
|
35
drawing/curve_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
type point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
type mockLine struct {
|
||||
inner []point
|
||||
}
|
||||
|
||||
func (ml *mockLine) LineTo(x, y float64) {
|
||||
ml.inner = append(ml.inner, point{x, y})
|
||||
}
|
||||
|
||||
func (ml mockLine) Len() int {
|
||||
return len(ml.inner)
|
||||
}
|
||||
|
||||
func TestTraceQuad(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Quad
|
||||
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||
// do the 9->12 circle segment
|
||||
quad := []float64{10, 20, 20, 20, 20, 10}
|
||||
liner := &mockLine{}
|
||||
TraceQuad(liner, quad, 0.5)
|
||||
assert.NotZero(liner.Len())
|
||||
}
|
|
@ -3,7 +3,7 @@ package drawing
|
|||
import (
|
||||
"image/color"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// FillRule defines the type for fill rules
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"git.fireandbrimst.one/aw/golang-freetype/raster"
|
||||
"git.fireandbrimst.one/aw/golang-image/math/fixed"
|
||||
"github.com/golang/freetype/raster"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// FtLineBuilder is a builder for freetype raster glyphs.
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"image"
|
||||
"image/color"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"image"
|
||||
"image/color"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-image/draw"
|
||||
"git.fireandbrimst.one/aw/golang-image/math/f64"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/math/f64"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/raster"
|
||||
"github.com/golang/freetype/raster"
|
||||
)
|
||||
|
||||
// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter
|
||||
|
|
|
@ -6,11 +6,11 @@ import (
|
|||
"image/color"
|
||||
"math"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/raster"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"git.fireandbrimst.one/aw/golang-image/draw"
|
||||
"git.fireandbrimst.one/aw/golang-image/font"
|
||||
"git.fireandbrimst.one/aw/golang-image/math/fixed"
|
||||
"github.com/golang/freetype/raster"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// NewRasterGraphicContext creates a new Graphic context from an image.
|
||||
|
@ -206,7 +206,7 @@ func (rgc *RasterGraphicContext) GetFont() *truetype.Font {
|
|||
return rgc.current.Font
|
||||
}
|
||||
|
||||
// SetFontSize sets the font size in points (as in “a 12 point font”).
|
||||
// SetFontSize sets the font size in points (as in ``a 12 point font'').
|
||||
func (rgc *RasterGraphicContext) SetFontSize(fontSizePoints float64) {
|
||||
rgc.current.FontSizePoints = fontSizePoints
|
||||
rgc.recalc()
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"image"
|
||||
"image/color"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// StackGraphicContext is a context that does thngs.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"git.fireandbrimst.one/aw/golang-image/math/fixed"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// DrawContour draws the given closed contour at the given sub-pixel offset.
|
||||
|
|
|
@ -3,10 +3,10 @@ package drawing
|
|||
import (
|
||||
"math"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-image/math/fixed"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"git.fireandbrimst.one/aw/golang-freetype/raster"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/raster"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// PixelsToPoints returns the points for a given number of pixels at a DPI.
|
||||
|
|
|
@ -7,13 +7,6 @@ const (
|
|||
DefaultEMAPeriod = 12
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*EMASeries)(nil)
|
||||
_ FirstValuesProvider = (*EMASeries)(nil)
|
||||
_ LastValuesProvider = (*EMASeries)(nil)
|
||||
)
|
||||
|
||||
// EMASeries is a computed series.
|
||||
type EMASeries struct {
|
||||
Name string
|
||||
|
@ -21,7 +14,7 @@ type EMASeries struct {
|
|||
YAxis YAxisType
|
||||
|
||||
Period int
|
||||
InnerSeries ValuesProvider
|
||||
InnerSeries ValueProvider
|
||||
|
||||
cache []float64
|
||||
}
|
||||
|
@ -59,36 +52,23 @@ func (ema EMASeries) GetSigma() float64 {
|
|||
return 2.0 / (float64(ema.GetPeriod()) + 1)
|
||||
}
|
||||
|
||||
// GetValues gets a value at a given index.
|
||||
func (ema *EMASeries) GetValues(index int) (x, y float64) {
|
||||
// GetValue gets a value at a given index.
|
||||
func (ema *EMASeries) GetValue(index int) (x, y float64) {
|
||||
if ema.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
if len(ema.cache) == 0 {
|
||||
ema.ensureCachedValues()
|
||||
}
|
||||
vx, _ := ema.InnerSeries.GetValues(index)
|
||||
vx, _ := ema.InnerSeries.GetValue(index)
|
||||
x = vx
|
||||
y = ema.cache[index]
|
||||
return
|
||||
}
|
||||
|
||||
// GetFirstValues computes the first moving average value.
|
||||
func (ema *EMASeries) GetFirstValues() (x, y float64) {
|
||||
if ema.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
if len(ema.cache) == 0 {
|
||||
ema.ensureCachedValues()
|
||||
}
|
||||
x, _ = ema.InnerSeries.GetValues(0)
|
||||
y = ema.cache[0]
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValues computes the last moving average value but walking back window size samples,
|
||||
// GetLastValue computes the last moving average value but walking back window size samples,
|
||||
// and recomputing the last moving average chunk.
|
||||
func (ema *EMASeries) GetLastValues() (x, y float64) {
|
||||
func (ema *EMASeries) GetLastValue() (x, y float64) {
|
||||
if ema.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
|
@ -96,7 +76,7 @@ func (ema *EMASeries) GetLastValues() (x, y float64) {
|
|||
ema.ensureCachedValues()
|
||||
}
|
||||
lastIndex := ema.InnerSeries.Len() - 1
|
||||
x, _ = ema.InnerSeries.GetValues(lastIndex)
|
||||
x, _ = ema.InnerSeries.GetValue(lastIndex)
|
||||
y = ema.cache[lastIndex]
|
||||
return
|
||||
}
|
||||
|
@ -106,7 +86,7 @@ func (ema *EMASeries) ensureCachedValues() {
|
|||
ema.cache = make([]float64, seriesLength)
|
||||
sigma := ema.GetSigma()
|
||||
for x := 0; x < seriesLength; x++ {
|
||||
_, y := ema.InnerSeries.GetValues(x)
|
||||
_, y := ema.InnerSeries.GetValue(x)
|
||||
if x == 0 {
|
||||
ema.cache[x] = y
|
||||
continue
|
||||
|
|
105
ema_series_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
var (
|
||||
emaXValues = Sequence.Float64(1.0, 50.0)
|
||||
emaYValues = []float64{
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2,
|
||||
}
|
||||
emaExpected = []float64{
|
||||
1,
|
||||
1.074074074,
|
||||
1.216735254,
|
||||
1.422903013,
|
||||
1.68787316,
|
||||
1.859141815,
|
||||
1.943649828,
|
||||
1.947823915,
|
||||
1.877614736,
|
||||
1.886680311,
|
||||
1.969148437,
|
||||
2.119581886,
|
||||
2.33294619,
|
||||
2.456431658,
|
||||
2.496695979,
|
||||
2.459903685,
|
||||
2.351762671,
|
||||
2.325706177,
|
||||
2.375653867,
|
||||
2.495975803,
|
||||
2.681459077,
|
||||
2.779128775,
|
||||
2.795489607,
|
||||
2.73656445,
|
||||
2.607930047,
|
||||
2.562898191,
|
||||
2.595276103,
|
||||
2.699329725,
|
||||
2.869749746,
|
||||
2.953471987,
|
||||
2.956918506,
|
||||
2.886035654,
|
||||
2.746329309,
|
||||
2.691045657,
|
||||
2.713931163,
|
||||
2.809195522,
|
||||
2.971477335,
|
||||
3.047664199,
|
||||
3.044133518,
|
||||
2.966790294,
|
||||
2.821102124,
|
||||
2.760279745,
|
||||
2.778036801,
|
||||
2.868552593,
|
||||
3.026437586,
|
||||
3.098553321,
|
||||
3.091253075,
|
||||
3.010419514,
|
||||
2.86149955,
|
||||
2.797684768,
|
||||
}
|
||||
emaDelta = 0.0001
|
||||
)
|
||||
|
||||
func TestEMASeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
mockSeries := mockValueProvider{
|
||||
emaXValues,
|
||||
emaYValues,
|
||||
}
|
||||
assert.Equal(50, mockSeries.Len())
|
||||
|
||||
ema := &EMASeries{
|
||||
InnerSeries: mockSeries,
|
||||
Period: 26,
|
||||
}
|
||||
|
||||
sig := ema.GetSigma()
|
||||
assert.Equal(2.0/(26.0+1), sig)
|
||||
|
||||
var yvalues []float64
|
||||
for x := 0; x < ema.Len(); x++ {
|
||||
_, y := ema.GetValue(x)
|
||||
yvalues = append(yvalues, y)
|
||||
}
|
||||
|
||||
for index, yv := range yvalues {
|
||||
assert.InDelta(yv, emaExpected[index], emaDelta)
|
||||
}
|
||||
|
||||
lvx, lvy := ema.GetLastValue()
|
||||
assert.Equal(50.0, lvx)
|
||||
assert.InDelta(lvy, emaExpected[49], emaDelta)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package chart
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -14,30 +14,24 @@ var (
|
|||
type fileUtil struct{}
|
||||
|
||||
// ReadByLines reads a file and calls the handler for each line.
|
||||
func (fu fileUtil) ReadByLines(filePath string, handler func(line string) error) error {
|
||||
var f *os.File
|
||||
var err error
|
||||
if f, err = os.Open(filePath); err == nil {
|
||||
func (fu fileUtil) ReadByLines(filePath string, handler func(line string)) error {
|
||||
if f, err := os.Open(filePath); err == nil {
|
||||
defer f.Close()
|
||||
var line string
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
err = handler(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line := scanner.Text()
|
||||
handler(line)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadByChunks reads a file in `chunkSize` pieces, dispatched to the handler.
|
||||
func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(line []byte) error) error {
|
||||
var f *os.File
|
||||
var err error
|
||||
if f, err = os.Open(filePath); err == nil {
|
||||
func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(line []byte)) error {
|
||||
if f, err := os.Open(filePath); err == nil {
|
||||
defer f.Close()
|
||||
|
||||
chunk := make([]byte, chunkSize)
|
||||
|
@ -47,11 +41,10 @@ func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(lin
|
|||
break
|
||||
}
|
||||
readData := chunk[:readBytes]
|
||||
err = handler(readData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler(readData)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package chart
|
||||
|
||||
import "fmt"
|
||||
|
||||
// FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation.
|
||||
func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
|
||||
var vf ValueFormatter
|
||||
if len(vfs) > 0 {
|
||||
vf = vfs[0]
|
||||
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
|
||||
_, vf = typed.GetValueFormatters()
|
||||
} else {
|
||||
vf = FloatValueFormatter
|
||||
}
|
||||
|
||||
var firstValue Value2
|
||||
if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped {
|
||||
firstValue.XValue, firstValue.YValue = typed.GetFirstValues()
|
||||
firstValue.Label = vf(firstValue.YValue)
|
||||
} else {
|
||||
firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0)
|
||||
firstValue.Label = vf(firstValue.YValue)
|
||||
}
|
||||
|
||||
var seriesName string
|
||||
var seriesStyle Style
|
||||
if typed, isTyped := innerSeries.(Series); isTyped {
|
||||
seriesName = fmt.Sprintf("%s - First Value", typed.GetName())
|
||||
seriesStyle = typed.GetStyle()
|
||||
}
|
||||
|
||||
return AnnotationSeries{
|
||||
Name: seriesName,
|
||||
Style: seriesStyle,
|
||||
Annotations: []Value2{firstValue},
|
||||
}
|
||||
}
|
29
font.go
|
@ -1,29 +0,0 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/roboto"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
)
|
||||
|
||||
var (
|
||||
_defaultFontLock sync.Mutex
|
||||
_defaultFont *truetype.Font
|
||||
)
|
||||
|
||||
// GetDefaultFont returns the default font (Roboto-Medium).
|
||||
func GetDefaultFont() (*truetype.Font, error) {
|
||||
if _defaultFont == nil {
|
||||
_defaultFontLock.Lock()
|
||||
defer _defaultFontLock.Unlock()
|
||||
if _defaultFont == nil {
|
||||
font, err := truetype.Parse(roboto.Roboto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_defaultFont = font
|
||||
}
|
||||
}
|
||||
return _defaultFont, nil
|
||||
}
|
24
grid_line_test.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestGenerateGridLines(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ticks := []Tick{
|
||||
{Value: 1.0, Label: "1.0"},
|
||||
{Value: 2.0, Label: "2.0"},
|
||||
{Value: 3.0, Label: "3.0"},
|
||||
{Value: 4.0, Label: "4.0"},
|
||||
}
|
||||
|
||||
gl := GenerateGridLines(ticks, Style{}, Style{})
|
||||
assert.Len(gl, 2)
|
||||
|
||||
assert.Equal(2.0, gl[0].Value)
|
||||
assert.Equal(3.0, gl[1].Value)
|
||||
}
|