Compare commits
12 Commits
master
...
candlestic
Author | SHA1 | Date | |
---|---|---|---|
|
de6df027fc | ||
|
e3e851d2d1 | ||
|
b537fd02cb | ||
|
5cf4f5f0d7 | ||
|
04a4edcb46 | ||
|
5936b89e89 | ||
|
51f3cca5d7 | ||
|
7ba2992824 | ||
|
7d1401898a | ||
|
e39acdfb76 | ||
|
566d798b32 | ||
|
73e3e439c5 |
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,19 +1,2 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Other
|
||||
.vscode
|
||||
.DS_Store
|
||||
coverage.html
|
13
.travis.yml
Normal file
13
.travis.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.8.1
|
||||
|
||||
sudo: false
|
||||
|
||||
before_script:
|
||||
- go get -u github.com/blendlabs/go-assert
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
- go test ./...
|
9
Makefile
Normal file
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
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) {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"},
|
||||
|
|
Binary file not shown.
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))
|
||||
}
|
Binary file not shown.
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) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func random(min, max float64) float64 {
|
||||
|
|
82
_examples/candlestick_series/main.go
Normal file
82
_examples/candlestick_series/main.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
chart "github.com/wcharczuk/go-chart"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func stockData() (times []time.Time, prices []float64) {
|
||||
start := time.Date(2017, 05, 15, 9, 30, 0, 0, util.Date.Eastern())
|
||||
price := 256.0
|
||||
for day := 0; day < 60; day++ {
|
||||
cursor := start.AddDate(0, 0, day)
|
||||
|
||||
if util.Date.IsNYSEHoliday(cursor) {
|
||||
continue
|
||||
}
|
||||
|
||||
for minute := 0; minute < ((6 * 60) + 30); minute++ {
|
||||
cursor = cursor.Add(time.Minute)
|
||||
|
||||
if rand.Float64() >= 0.5 {
|
||||
price = price + (rand.Float64() * (price * 0.01))
|
||||
} else {
|
||||
price = price - (rand.Float64() * (price * 0.01))
|
||||
}
|
||||
|
||||
times = append(times, cursor)
|
||||
prices = append(prices, price)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
xv, yv := stockData()
|
||||
|
||||
priceSeries := chart.TimeSeries{
|
||||
Name: "SPY",
|
||||
Style: chart.Style{
|
||||
Show: false,
|
||||
StrokeColor: chart.GetDefaultColor(0),
|
||||
},
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
}
|
||||
|
||||
candleSeries := chart.CandlestickSeries{
|
||||
Name: "SPY",
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{Show: true, FontSize: 8, TextRotationDegrees: 45},
|
||||
TickPosition: chart.TickPositionUnderTick,
|
||||
Range: &chart.MarketHoursRange{},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{Show: true},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
candleSeries,
|
||||
priceSeries,
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := graph.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -3,9 +3,9 @@ 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"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -20,10 +20,14 @@ 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{
|
||||
|
@ -43,10 +47,14 @@ 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{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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,8 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -16,8 +16,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: seq.RandomValuesWithAverage(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
// note we create a LinearRegressionSeries series by assignin the inner series.
|
||||
|
|
46
_examples/market_hours/main.go
Normal file
46
_examples/market_hours/main.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
start := util.Date.Date(2016, 7, 01, util.Date.Eastern())
|
||||
end := util.Date.Date(2016, 07, 21, util.Date.Eastern())
|
||||
xv := seq.Time.MarketHours(start, end, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday)
|
||||
yv := seq.New(seq.NewRandom().WithLen(len(xv)).WithAverage(200).WithScale(10)).Array()
|
||||
|
||||
graph := chart.Chart{
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.StyleShow(),
|
||||
TickPosition: chart.TickPositionBetweenTicks,
|
||||
ValueFormatter: chart.TimeHourValueFormatter,
|
||||
Range: &chart.MarketHoursRange{
|
||||
MarketOpen: util.NYSEOpen(),
|
||||
MarketClose: util.NYSEClose(),
|
||||
HolidayProvider: util.Date.IsNYSEHoliday,
|
||||
},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.StyleShow(),
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.TimeSeries{
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
graph.Render(chart.PNG, res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
BIN
_examples/market_hours/output.png
Normal file
BIN
_examples/market_hours/output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
|
@ -3,15 +3,15 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
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(),
|
||||
YValues: seq.New(seq.NewRandom().WithLen(100).WithAverage(100).WithScale(50)).Array(),
|
||||
}
|
||||
|
||||
minSeries := &chart.MinSeries{
|
||||
|
|
49
_examples/overlap/main.go
Normal file
49
_examples/overlap/main.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
chart "github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func conditionalColor(condition bool, trueColor drawing.Color, falseColor drawing.Color) drawing.Color {
|
||||
if condition {
|
||||
return trueColor
|
||||
}
|
||||
return falseColor
|
||||
}
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
r, _ := chart.PNG(1024, 1024)
|
||||
|
||||
b0 := chart.Box{Left: 100, Top: 100, Right: 400, Bottom: 200}
|
||||
b1 := chart.Box{Left: 500, Top: 100, Right: 900, Bottom: 200}
|
||||
b0r := b0.Corners().Rotate(45).Shift(0, 200)
|
||||
|
||||
chart.Draw.Box(r, b0, chart.Style{
|
||||
StrokeColor: drawing.ColorRed,
|
||||
StrokeWidth: 2,
|
||||
FillColor: conditionalColor(b0.Corners().Overlaps(b1.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||
})
|
||||
|
||||
chart.Draw.Box(r, b1, chart.Style{
|
||||
StrokeColor: drawing.ColorBlue,
|
||||
StrokeWidth: 2,
|
||||
FillColor: conditionalColor(b1.Corners().Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||
})
|
||||
|
||||
chart.Draw.Box2d(r, b0r, chart.Style{
|
||||
StrokeColor: drawing.ColorGreen,
|
||||
StrokeWidth: 2,
|
||||
FillColor: conditionalColor(b0r.Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||
})
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
r.Save(res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -3,8 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -16,8 +16,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
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: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||
YValues: seq.RandomValuesWithAverage(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||
}
|
||||
|
||||
polyRegSeries := &chart.PolynomialRegressionSeries{
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func parseInt(str string) int {
|
||||
|
@ -105,7 +105,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,
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -6,9 +6,9 @@ import (
|
|||
|
||||
_ "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"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -44,10 +44,10 @@ 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{
|
||||
|
|
|
@ -3,8 +3,8 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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) {
|
||||
|
@ -32,11 +32,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
tbc := tb.Corners().Rotate(45)
|
||||
|
||||
chart.Draw.BoxCorners(r, tbc, chart.Style{
|
||||
chart.Draw.Box2d(r, tbc, chart.Style{
|
||||
StrokeColor: drawing.ColorRed,
|
||||
StrokeWidth: 2,
|
||||
})
|
||||
|
||||
tbc2 := tbc.Shift(tbc.Height(), 0)
|
||||
chart.Draw.Box2d(r, tbc2, chart.Style{
|
||||
StrokeColor: drawing.ColorGreen,
|
||||
StrokeWidth: 2,
|
||||
})
|
||||
|
||||
tbcb := tbc.Box()
|
||||
chart.Draw.Box(r, tbcb, chart.Style{
|
||||
StrokeColor: drawing.ColorBlue,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
|
@ -18,7 +18,9 @@ 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)
|
||||
|
@ -27,10 +29,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
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{
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart"
|
||||
"github.com/wcharczuk/go-chart"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -14,8 +14,10 @@ func main() {
|
|||
b = 1000
|
||||
|
||||
ts1 := chart.ContinuousSeries{ //TimeSeries{
|
||||
Name: "Time Series",
|
||||
Style: chart.StyleShow(),
|
||||
Name: "Time Series",
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
},
|
||||
XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b},
|
||||
YValues: []float64{1.0, 2.0, 30.0, 4.0, 50.0, 6.0, 7.0, 88.0},
|
||||
}
|
||||
|
|
|
@ -4,12 +4,7 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*AnnotationSeries)(nil)
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// AnnotationSeries is a series of labels on the chart.
|
||||
|
|
119
annotation_series_test.go
Normal file
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)
|
||||
}
|
69
bar_chart.go
69
bar_chart.go
|
@ -6,8 +6,8 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// BarChart is a chart that draws bars on a range.
|
||||
|
@ -31,9 +31,6 @@ type BarChart struct {
|
|||
|
||||
BarSpacing int
|
||||
|
||||
UseBaseValue bool
|
||||
BaseValue float64
|
||||
|
||||
Font *truetype.Font
|
||||
defaultFont *truetype.Font
|
||||
|
||||
|
@ -129,7 +126,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
|
||||
yr = bc.setRangeDomains(canvasBox, yr)
|
||||
}
|
||||
bc.drawCanvas(r, canvasBox)
|
||||
|
||||
bc.drawBars(r, canvasBox, yr)
|
||||
bc.drawXAxis(r, canvasBox)
|
||||
bc.drawYAxis(r, canvasBox, yr, yt)
|
||||
|
@ -142,10 +139,6 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
|||
return r.Save(w)
|
||||
}
|
||||
|
||||
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||
Draw.Box(r, canvasBox, bc.getCanvasStyle())
|
||||
}
|
||||
|
||||
func (bc BarChart) getRanges() Range {
|
||||
var yrange Range
|
||||
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
|
||||
|
@ -202,20 +195,11 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
|||
|
||||
by = canvasBox.Bottom - yr.Translate(bar.Value)
|
||||
|
||||
if bc.UseBaseValue {
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
|
||||
}
|
||||
} else {
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom,
|
||||
}
|
||||
barBox = Box{
|
||||
Top: by,
|
||||
Left: bxl,
|
||||
Right: bxr,
|
||||
Bottom: canvasBox.Bottom,
|
||||
}
|
||||
|
||||
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
|
||||
|
@ -277,7 +261,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
|||
r.Stroke()
|
||||
|
||||
var ty int
|
||||
var tb Box
|
||||
var tb Box2d
|
||||
for _, t := range ticks {
|
||||
ty = canvasBox.Bottom - yr.Translate(t.Value)
|
||||
|
||||
|
@ -288,7 +272,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
|||
|
||||
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||
tb = r.MeasureText(t.Label)
|
||||
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
||||
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(int(tb.Height())>>1), axisStyle)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -296,32 +280,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 +369,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
|||
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||
|
||||
xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
xaxisHeight = util.Math.MinInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -438,8 +397,8 @@ func (bc BarChart) box() Box {
|
|||
dpb := bc.Background.Padding.GetBottom(50)
|
||||
|
||||
return Box{
|
||||
Top: bc.Background.Padding.GetTop(20),
|
||||
Left: bc.Background.Padding.GetLeft(20),
|
||||
Top: 20,
|
||||
Left: 20,
|
||||
Right: bc.GetWidth() - dpr,
|
||||
Bottom: bc.GetHeight() - dpb,
|
||||
}
|
||||
|
|
320
bar_chart_test.go
Normal file
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)
|
||||
}
|
|
@ -3,12 +3,7 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*BollingerBandsSeries)(nil)
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
// BollingerBandsSeries draws bollinger bands for an inner series.
|
||||
|
|
53
bollinger_band_series_test.go
Normal file
53
bollinger_band_series_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func TestBollingerBandSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValuesProvider{
|
||||
X: seq.Range(1.0, 100.0),
|
||||
Y: seq.RandomValuesWithMax(100, 1024),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
InnerSeries: s1,
|
||||
}
|
||||
|
||||
xvalues := make([]float64, 100)
|
||||
y1values := make([]float64, 100)
|
||||
y2values := make([]float64, 100)
|
||||
|
||||
for x := 0; x < 100; x++ {
|
||||
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x)
|
||||
}
|
||||
|
||||
for x := bbs.GetPeriod(); x < 100; x++ {
|
||||
assert.True(y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBollingerBandLastValue(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValuesProvider{
|
||||
X: seq.Range(1.0, 100.0),
|
||||
Y: seq.Range(1.0, 100.0),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
InnerSeries: s1,
|
||||
}
|
||||
|
||||
x, y1, y2 := bbs.GetBoundedLastValues()
|
||||
assert.Equal(100.0, x)
|
||||
assert.Equal(101, math.Floor(y1))
|
||||
assert.Equal(83, math.Floor(y2))
|
||||
}
|
111
box.go
111
box.go
|
@ -2,9 +2,8 @@ package chart
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -166,12 +165,12 @@ func (b Box) Shift(x, y int) Box {
|
|||
}
|
||||
|
||||
// Corners returns the box as a set of corners.
|
||||
func (b Box) Corners() BoxCorners {
|
||||
return BoxCorners{
|
||||
TopLeft: Point{b.Left, b.Top},
|
||||
TopRight: Point{b.Right, b.Top},
|
||||
BottomRight: Point{b.Right, b.Bottom},
|
||||
BottomLeft: Point{b.Left, b.Bottom},
|
||||
func (b Box) Corners() Box2d {
|
||||
return Box2d{
|
||||
TopLeft: Point{float64(b.Left), float64(b.Top)},
|
||||
TopRight: Point{float64(b.Right), float64(b.Top)},
|
||||
BottomRight: Point{float64(b.Right), float64(b.Bottom)},
|
||||
BottomLeft: Point{float64(b.Left), float64(b.Bottom)},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,99 +254,3 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
|
|||
}
|
||||
return newBox
|
||||
}
|
||||
|
||||
// BoxCorners is a box with independent corners.
|
||||
type BoxCorners struct {
|
||||
TopLeft, TopRight, BottomRight, BottomLeft Point
|
||||
}
|
||||
|
||||
// Box return the BoxCorners as a regular box.
|
||||
func (bc BoxCorners) Box() Box {
|
||||
return Box{
|
||||
Top: 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),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
y = ((bottom - top) >> 1) + top
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate rotates the box.
|
||||
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
|
||||
cx, cy := bc.Center()
|
||||
|
||||
thetaRadians := util.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)
|
||||
|
||||
return BoxCorners{
|
||||
TopLeft: Point{tlx, tly},
|
||||
TopRight: Point{trx, try},
|
||||
BottomRight: Point{brx, bry},
|
||||
BottomLeft: Point{blx, bly},
|
||||
}
|
||||
}
|
||||
|
||||
// Equals returns if the box equals another box.
|
||||
func (bc BoxCorners) Equals(other BoxCorners) bool {
|
||||
return bc.TopLeft.Equals(other.TopLeft) &&
|
||||
bc.TopRight.Equals(other.TopRight) &&
|
||||
bc.BottomRight.Equals(other.BottomRight) &&
|
||||
bc.BottomLeft.Equals(other.BottomLeft)
|
||||
}
|
||||
|
||||
func (bc BoxCorners) String() string {
|
||||
return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
|
||||
}
|
||||
|
||||
// Point is an X,Y pair
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// DistanceTo calculates the distance to another point.
|
||||
func (p Point) DistanceTo(other Point) float64 {
|
||||
dx := math.Pow(float64(p.X-other.X), 2)
|
||||
dy := math.Pow(float64(p.Y-other.Y), 2)
|
||||
return math.Pow(dx+dy, 0.5)
|
||||
}
|
||||
|
||||
// Equals returns if a point equals another point.
|
||||
func (p Point) Equals(other Point) bool {
|
||||
return p.X == other.X && p.Y == other.Y
|
||||
}
|
||||
|
||||
// String returns a string representation of the point.
|
||||
func (p Point) String() string {
|
||||
return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
|
||||
}
|
||||
|
|
183
box_2d.go
Normal file
183
box_2d.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// Box2d is a box with (4) independent corners.
|
||||
// It is used when dealing with ~rotated~ boxes.
|
||||
type Box2d struct {
|
||||
TopLeft, TopRight, BottomRight, BottomLeft Point
|
||||
}
|
||||
|
||||
// Points returns the constituent points of the box.
|
||||
func (bc Box2d) Points() []Point {
|
||||
return []Point{
|
||||
bc.TopRight,
|
||||
bc.BottomRight,
|
||||
bc.BottomLeft,
|
||||
bc.TopLeft,
|
||||
}
|
||||
}
|
||||
|
||||
// Box return the Box2d as a regular box.
|
||||
func (bc Box2d) Box() Box {
|
||||
return Box{
|
||||
Top: int(bc.Top()),
|
||||
Left: int(bc.Left()),
|
||||
Right: int(bc.Right()),
|
||||
Bottom: int(bc.Bottom()),
|
||||
}
|
||||
}
|
||||
|
||||
// Top returns the top-most corner y value.
|
||||
func (bc Box2d) Top() float64 {
|
||||
return math.Min(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
}
|
||||
|
||||
// Left returns the left-most corner x value.
|
||||
func (bc Box2d) Left() float64 {
|
||||
return math.Min(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
}
|
||||
|
||||
// Right returns the right-most corner x value.
|
||||
func (bc Box2d) Right() float64 {
|
||||
return math.Max(bc.TopRight.X, bc.BottomRight.X)
|
||||
}
|
||||
|
||||
// Bottom returns the bottom-most corner y value.
|
||||
func (bc Box2d) Bottom() float64 {
|
||||
return math.Max(bc.BottomLeft.Y, bc.BottomLeft.Y)
|
||||
}
|
||||
|
||||
// Width returns the width
|
||||
func (bc Box2d) Width() float64 {
|
||||
minLeft := math.Min(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
maxRight := math.Max(bc.TopRight.X, bc.BottomRight.X)
|
||||
return maxRight - minLeft
|
||||
}
|
||||
|
||||
// Height returns the height
|
||||
func (bc Box2d) Height() float64 {
|
||||
minTop := math.Min(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
maxBottom := math.Max(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
return maxBottom - minTop
|
||||
}
|
||||
|
||||
// Center returns the center of the box
|
||||
func (bc Box2d) Center() (x, y float64) {
|
||||
left := util.Math.Mean(bc.TopLeft.X, bc.BottomLeft.X)
|
||||
right := util.Math.Mean(bc.TopRight.X, bc.BottomRight.X)
|
||||
x = ((right - left) / 2.0) + left
|
||||
|
||||
top := util.Math.Mean(bc.TopLeft.Y, bc.TopRight.Y)
|
||||
bottom := util.Math.Mean(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||
y = ((bottom - top) / 2.0) + top
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate rotates the box.
|
||||
func (bc Box2d) Rotate(thetaDegrees float64) Box2d {
|
||||
cx, cy := bc.Center()
|
||||
|
||||
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
|
||||
|
||||
tlx, tly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopLeft.X), int(bc.TopLeft.Y), thetaRadians)
|
||||
trx, try := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopRight.X), int(bc.TopRight.Y), thetaRadians)
|
||||
brx, bry := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomRight.X), int(bc.BottomRight.Y), thetaRadians)
|
||||
blx, bly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomLeft.X), int(bc.BottomLeft.Y), thetaRadians)
|
||||
|
||||
return Box2d{
|
||||
TopLeft: Point{float64(tlx), float64(tly)},
|
||||
TopRight: Point{float64(trx), float64(try)},
|
||||
BottomRight: Point{float64(brx), float64(bry)},
|
||||
BottomLeft: Point{float64(blx), float64(bly)},
|
||||
}
|
||||
}
|
||||
|
||||
// Shift shifts a box by a given x and y value.
|
||||
func (bc Box2d) Shift(x, y float64) Box2d {
|
||||
return Box2d{
|
||||
TopLeft: bc.TopLeft.Shift(x, y),
|
||||
TopRight: bc.TopRight.Shift(x, y),
|
||||
BottomRight: bc.BottomRight.Shift(x, y),
|
||||
BottomLeft: bc.BottomLeft.Shift(x, y),
|
||||
}
|
||||
}
|
||||
|
||||
// Equals returns if the box equals another box.
|
||||
func (bc Box2d) Equals(other Box2d) bool {
|
||||
return bc.TopLeft.Equals(other.TopLeft) &&
|
||||
bc.TopRight.Equals(other.TopRight) &&
|
||||
bc.BottomRight.Equals(other.BottomRight) &&
|
||||
bc.BottomLeft.Equals(other.BottomLeft)
|
||||
}
|
||||
|
||||
// Overlaps returns if two boxes overlap.
|
||||
func (bc Box2d) Overlaps(other Box2d) bool {
|
||||
pa := bc.Points()
|
||||
pb := other.Points()
|
||||
for i := 0; i < 4; i++ {
|
||||
for j := 0; j < 4; j++ {
|
||||
pa0 := pa[i]
|
||||
pa1 := pa[(i+1)%4]
|
||||
|
||||
pb0 := pb[j]
|
||||
pb1 := pb[(j+1)%4]
|
||||
|
||||
if util.Math.LinesIntersect(pa0.X, pa0.Y, pa1.X, pa1.Y, pb0.X, pb0.Y, pb1.X, pb1.Y) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Grow grows a box by a given set of dimensions.
|
||||
func (bc Box2d) Grow(by Box) Box2d {
|
||||
top, left, right, bottom := float64(by.Top), float64(by.Left), float64(by.Right), float64(by.Bottom)
|
||||
return Box2d{
|
||||
TopLeft: Point{X: bc.TopLeft.X - left, Y: bc.TopLeft.Y - top},
|
||||
TopRight: Point{X: bc.TopRight.X + right, Y: bc.TopRight.Y - top},
|
||||
BottomRight: Point{X: bc.BottomRight.X + right, Y: bc.BottomRight.Y + bottom},
|
||||
BottomLeft: Point{X: bc.BottomLeft.X - left, Y: bc.BottomLeft.Y + bottom},
|
||||
}
|
||||
}
|
||||
|
||||
func (bc Box2d) String() string {
|
||||
return fmt.Sprintf("Box2d{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
|
||||
}
|
||||
|
||||
// Point is an X,Y pair
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// Shift shifts a point.
|
||||
func (p Point) Shift(x, y float64) Point {
|
||||
return Point{
|
||||
X: p.X + x,
|
||||
Y: p.Y + y,
|
||||
}
|
||||
}
|
||||
|
||||
// DistanceTo calculates the distance to another point.
|
||||
func (p Point) DistanceTo(other Point) float64 {
|
||||
dx := math.Pow(p.X-other.X, 2)
|
||||
dy := math.Pow(p.Y-other.Y, 2)
|
||||
return math.Pow(dx+dy, 0.5)
|
||||
}
|
||||
|
||||
// Equals returns if a point equals another point.
|
||||
func (p Point) Equals(other Point) bool {
|
||||
return p.X == other.X && p.Y == other.Y
|
||||
}
|
||||
|
||||
// String returns a string representation of the point.
|
||||
func (p Point) String() string {
|
||||
return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y)
|
||||
}
|
66
box_2d_test.go
Normal file
66
box_2d_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestBox2dCenter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := Box2d{
|
||||
TopLeft: Point{5, 5},
|
||||
TopRight: Point{15, 5},
|
||||
BottomRight: Point{15, 15},
|
||||
BottomLeft: Point{5, 15},
|
||||
}
|
||||
|
||||
cx, cy := bc.Center()
|
||||
assert.Equal(10, cx)
|
||||
assert.Equal(10, cy)
|
||||
}
|
||||
|
||||
func TestBox2dRotate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := Box2d{
|
||||
TopLeft: Point{5, 5},
|
||||
TopRight: Point{15, 5},
|
||||
BottomRight: Point{15, 15},
|
||||
BottomLeft: Point{5, 15},
|
||||
}
|
||||
|
||||
rotated := bc.Rotate(45)
|
||||
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
||||
}
|
||||
|
||||
func TestBox2dOverlaps(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
bc := Box2d{
|
||||
TopLeft: Point{5, 5},
|
||||
TopRight: Point{15, 5},
|
||||
BottomRight: Point{15, 15},
|
||||
BottomLeft: Point{5, 15},
|
||||
}
|
||||
|
||||
// shift meaningfully the full width of bc right.
|
||||
bc2 := bc.Shift(bc.Width()+1, 0)
|
||||
assert.False(bc.Overlaps(bc2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc2))
|
||||
|
||||
// shift meaningfully the full height of bc down.
|
||||
bc3 := bc.Shift(0, bc.Height()+1)
|
||||
assert.False(bc.Overlaps(bc3), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc3))
|
||||
|
||||
bc4 := bc.Shift(5, 0)
|
||||
assert.True(bc.Overlaps(bc4))
|
||||
|
||||
bc5 := bc.Shift(0, 5)
|
||||
assert.True(bc.Overlaps(bc5))
|
||||
|
||||
bcr := bc.Rotate(45)
|
||||
bcr2 := bc.Rotate(45).Shift(bc.Width()/2.0, 0)
|
||||
assert.True(bcr.Overlaps(bcr2), fmt.Sprintf("%v\n\t\tshould overlap\n\t%v", bcr, bcr2))
|
||||
}
|
159
box_test.go
Normal file
159
box_test.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
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)
|
||||
}
|
157
candlestick_series.go
Normal file
157
candlestick_series.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"math"
|
||||
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// CandleValue is a day's data for a candlestick plot.
|
||||
type CandleValue struct {
|
||||
Timestamp time.Time
|
||||
High float64
|
||||
Low float64
|
||||
Open float64
|
||||
Close float64
|
||||
}
|
||||
|
||||
// String returns a string value for the candle value.
|
||||
func (cv CandleValue) String() string {
|
||||
return fmt.Sprintf("candle %s high: %.2f low: %.2f open: %.2f close: %.2f", cv.Timestamp.Format("2006-01-02"), cv.High, cv.Low, cv.Open, cv.Close)
|
||||
}
|
||||
|
||||
// IsZero returns if the value is zero or not.
|
||||
func (cv CandleValue) IsZero() bool {
|
||||
return cv.Timestamp.IsZero()
|
||||
}
|
||||
|
||||
// CandlestickSeries is a special type of series that takes a norma value provider
|
||||
// and maps it to day value stats (high, low, open, close).
|
||||
type CandlestickSeries struct {
|
||||
Name string
|
||||
Style Style
|
||||
YAxis YAxisType
|
||||
|
||||
// CandleValues will be used in place of creating them from the `InnerSeries`.
|
||||
CandleValues []CandleValue
|
||||
|
||||
// InnerSeries is used if the `CandleValues` are not set.
|
||||
InnerSeries ValuesProvider
|
||||
}
|
||||
|
||||
// GetName implements Series.GetName.
|
||||
func (cs *CandlestickSeries) GetName() string {
|
||||
return cs.Name
|
||||
}
|
||||
|
||||
// GetStyle implements Series.GetStyle.
|
||||
func (cs *CandlestickSeries) GetStyle() Style {
|
||||
return cs.Style
|
||||
}
|
||||
|
||||
// GetYAxis returns which yaxis the series is mapped to.
|
||||
func (cs *CandlestickSeries) GetYAxis() YAxisType {
|
||||
return cs.YAxis
|
||||
}
|
||||
|
||||
// Len returns the length of the series.
|
||||
func (cs *CandlestickSeries) Len() int {
|
||||
return len(cs.GetCandleValues())
|
||||
}
|
||||
|
||||
// GetBoundedValues returns the bounded values at a given index.
|
||||
func (cs *CandlestickSeries) GetBoundedValues(index int) (x, y0, y1 float64) {
|
||||
value := cs.GetCandleValues()[index]
|
||||
return util.Time.ToFloat64(value.Timestamp), value.Low, value.High
|
||||
}
|
||||
|
||||
// GetCandleValues returns the candle values.
|
||||
func (cs CandlestickSeries) GetCandleValues() []CandleValue {
|
||||
if cs.CandleValues == nil {
|
||||
cs.CandleValues = cs.GenerateCandleValues()
|
||||
}
|
||||
return cs.CandleValues
|
||||
}
|
||||
|
||||
// GenerateCandleValues returns the candlestick values for each day represented by the inner series.
|
||||
func (cs CandlestickSeries) GenerateCandleValues() []CandleValue {
|
||||
if cs.InnerSeries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalValues := cs.InnerSeries.Len()
|
||||
if totalValues == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var values []CandleValue
|
||||
var lastYear, lastMonth, lastDay int
|
||||
var year, month, day int
|
||||
|
||||
var t time.Time
|
||||
var tv, lv, v float64
|
||||
|
||||
tv, v = cs.InnerSeries.GetValues(0)
|
||||
t = util.Time.FromFloat64(tv)
|
||||
year, month, day = t.Year(), int(t.Month()), t.Day()
|
||||
|
||||
lastYear, lastMonth, lastDay = year, month, day
|
||||
|
||||
value := CandleValue{
|
||||
Timestamp: cs.newTimestamp(year, month, day),
|
||||
Open: v,
|
||||
Low: v,
|
||||
High: v,
|
||||
}
|
||||
lv = v
|
||||
|
||||
for i := 1; i < totalValues; i++ {
|
||||
tv, v = cs.InnerSeries.GetValues(i)
|
||||
t = util.Time.FromFloat64(tv)
|
||||
year, month, day = t.Year(), int(t.Month()), t.Day()
|
||||
|
||||
// if we've transitioned to a new day or we're on the last value
|
||||
if lastYear != year || lastMonth != month || lastDay != day || i == (totalValues-1) {
|
||||
value.Close = lv
|
||||
values = append(values, value)
|
||||
|
||||
value = CandleValue{
|
||||
Timestamp: cs.newTimestamp(year, month, day),
|
||||
Open: v,
|
||||
High: v,
|
||||
Low: v,
|
||||
}
|
||||
|
||||
lastYear = year
|
||||
lastMonth = month
|
||||
lastDay = day
|
||||
} else {
|
||||
value.Low = math.Min(value.Low, v)
|
||||
value.High = math.Max(value.High, v)
|
||||
}
|
||||
lv = v
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time {
|
||||
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, util.Date.Eastern())
|
||||
}
|
||||
|
||||
// Render implements Series.Render.
|
||||
func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||
style := cs.Style.InheritFrom(defaults)
|
||||
Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (cs CandlestickSeries) Validate() error {
|
||||
if cs.CandleValues == nil && cs.InnerSeries == nil {
|
||||
return fmt.Errorf("candlestick series requires either `CandleValues` or `InnerSeries` to be set")
|
||||
}
|
||||
return nil
|
||||
}
|
52
candlestick_series_test.go
Normal file
52
candlestick_series_test.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func generateDummyStockData() (times []time.Time, prices []float64) {
|
||||
start := util.Date.On(util.NYSEOpen(), time.Date(2017, 05, 15, 0, 0, 0, 0, util.Date.Eastern()))
|
||||
cursor := start
|
||||
for day := 0; day < 60; day++ {
|
||||
|
||||
if util.Date.IsWeekendDay(cursor.Weekday()) {
|
||||
cursor = start.AddDate(0, 0, day)
|
||||
continue
|
||||
}
|
||||
|
||||
for hour := 0; hour < 7; hour++ {
|
||||
for minute := 0; minute < 60; minute++ {
|
||||
times = append(times, cursor)
|
||||
prices = append(prices, rand.Float64()*256)
|
||||
cursor = cursor.Add(time.Minute)
|
||||
}
|
||||
|
||||
cursor = cursor.Add(time.Hour)
|
||||
}
|
||||
|
||||
cursor = start.AddDate(0, 0, day)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestCandlestickSeriesCandleValues(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
xdata, ydata := generateDummyStockData()
|
||||
|
||||
candleSeries := &CandlestickSeries{
|
||||
InnerSeries: TimeSeries{
|
||||
XValues: xdata,
|
||||
YValues: ydata,
|
||||
},
|
||||
}
|
||||
|
||||
values := candleSeries.GetCandleValues()
|
||||
assert.Len(values, 43) // should be 60 days per the generator.
|
||||
}
|
14
chart.go
14
chart.go
|
@ -6,8 +6,8 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// Chart is what we're drawing.
|
||||
|
@ -317,6 +317,9 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
|
|||
if math.IsNaN(yDelta) {
|
||||
return errors.New("nan y-range delta")
|
||||
}
|
||||
if yDelta == 0 {
|
||||
return errors.New("zero y-range delta")
|
||||
}
|
||||
|
||||
if c.hasSecondarySeries() {
|
||||
yraDelta := yra.GetDelta()
|
||||
|
@ -326,6 +329,9 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
|
|||
if math.IsNaN(yraDelta) {
|
||||
return errors.New("nan secondary y-range delta")
|
||||
}
|
||||
if yraDelta == 0 {
|
||||
return errors.New("zero secondary y-range delta")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -496,8 +502,8 @@ func (c Chart) drawTitle(r Renderer) {
|
|||
textWidth := textBox.Width()
|
||||
textHeight := textBox.Height()
|
||||
|
||||
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
|
||||
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||
titleX := (int(c.GetWidth()) >> 1) - (int(textWidth) >> 1)
|
||||
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(textHeight)
|
||||
|
||||
r.Text(c.Title, titleX, titleY)
|
||||
}
|
||||
|
|
576
chart_test.go
Normal file
576
chart_test.go
Normal file
|
@ -0,0 +1,576 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
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: seq.Range(1.0, 10.0),
|
||||
YValues: seq.Range(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: seq.Range(1.0, 10.0),
|
||||
YValues: seq.Range(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(c.validateSeries())
|
||||
|
||||
c = Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: seq.Range(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: seq.RangeWithStep(0, 4, 1),
|
||||
YValues: seq.RangeWithStep(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: seq.RangeWithStep(0, 4, 1),
|
||||
YValues: seq.RangeWithStep(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))
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package chart
|
||||
|
||||
import "git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
import "github.com/wcharczuk/go-chart/drawing"
|
||||
|
||||
var (
|
||||
// ColorWhite is white.
|
||||
|
|
42
concat_series_test.go
Normal file
42
concat_series_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func TestConcatSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s1 := ContinuousSeries{
|
||||
XValues: seq.Range(1.0, 10.0),
|
||||
YValues: seq.Range(1.0, 10.0),
|
||||
}
|
||||
|
||||
s2 := ContinuousSeries{
|
||||
XValues: seq.Range(11, 20.0),
|
||||
YValues: seq.Range(10.0, 1.0),
|
||||
}
|
||||
|
||||
s3 := ContinuousSeries{
|
||||
XValues: seq.Range(21, 30.0),
|
||||
YValues: seq.Range(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)
|
||||
}
|
23
continuous_range_test.go
Normal file
23
continuous_range_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
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 = util.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
|
||||
|
@ -43,11 +36,6 @@ func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
|
|||
return cs.XValues[index], cs.YValues[index]
|
||||
}
|
||||
|
||||
// GetFirstValues gets the first x,y values.
|
||||
func (cs ContinuousSeries) GetFirstValues() (float64, float64) {
|
||||
return cs.XValues[0], cs.YValues[0]
|
||||
}
|
||||
|
||||
// GetLastValues gets the last x,y values.
|
||||
func (cs ContinuousSeries) GetLastValues() (float64, float64) {
|
||||
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
|
||||
|
|
73
continuous_series_test.go
Normal file
73
continuous_series_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func TestContinuousSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: seq.Range(1.0, 10.0),
|
||||
YValues: seq.Range(1.0, 10.0),
|
||||
}
|
||||
|
||||
assert.Equal("Test Series", cs.GetName())
|
||||
assert.Equal(10, cs.Len())
|
||||
x0, y0 := cs.GetValues(0)
|
||||
assert.Equal(1.0, x0)
|
||||
assert.Equal(1.0, y0)
|
||||
|
||||
xn, yn := cs.GetValues(9)
|
||||
assert.Equal(10.0, xn)
|
||||
assert.Equal(10.0, yn)
|
||||
|
||||
xn, yn = cs.GetLastValues()
|
||||
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: seq.Range(1.0, 10.0),
|
||||
YValues: seq.Range(1.0, 10.0),
|
||||
}
|
||||
assert.Nil(cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: seq.Range(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
|
||||
cs = ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
YValues: seq.Range(1.0, 10.0),
|
||||
}
|
||||
assert.NotNil(cs.Validate())
|
||||
}
|
BIN
debug.test
Executable file
BIN
debug.test
Executable file
Binary file not shown.
95
draw.go
95
draw.go
|
@ -3,7 +3,7 @@ package chart
|
|||
import (
|
||||
"math"
|
||||
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -168,14 +168,73 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
|||
}
|
||||
}
|
||||
|
||||
func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, cs CandlestickSeries) {
|
||||
if cs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
candleValues := cs.GetCandleValues()
|
||||
|
||||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
||||
var cv CandleValue
|
||||
for index := 0; index < len(candleValues); index++ {
|
||||
cv = candleValues[index]
|
||||
|
||||
y0 := yrange.Translate(cv.Open)
|
||||
y1 := yrange.Translate(cv.Close)
|
||||
|
||||
x0 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEOpen(), cv.Timestamp)))
|
||||
x1 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEClose(), cv.Timestamp)))
|
||||
|
||||
x := x0 + ((x1 - x0) >> 1)
|
||||
|
||||
// draw open / close box.
|
||||
if cv.Open < cv.Close {
|
||||
d.Box(r, Box{
|
||||
Top: cb - y0,
|
||||
Left: x0,
|
||||
Right: x1,
|
||||
Bottom: cb - y1,
|
||||
}, style.InheritFrom(Style{FillColor: ColorAlternateGreen}))
|
||||
} else {
|
||||
d.Box(r, Box{
|
||||
Top: cb - y1,
|
||||
Left: x0,
|
||||
Right: x1,
|
||||
Bottom: cb - y0,
|
||||
}, style.InheritFrom(Style{FillColor: ColorRed}))
|
||||
}
|
||||
|
||||
// draw high / low t bars
|
||||
y0 = yrange.Translate(cv.High)
|
||||
y1 = yrange.Translate(cv.Low)
|
||||
|
||||
style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r)
|
||||
|
||||
r.MoveTo(x0, cb-y0)
|
||||
r.LineTo(x1, cb-y0)
|
||||
r.Stroke()
|
||||
|
||||
r.MoveTo(x, cb-y0)
|
||||
r.LineTo(x, cb-y1)
|
||||
r.Stroke()
|
||||
|
||||
r.MoveTo(x0, cb-y1)
|
||||
r.LineTo(x1, cb-y1)
|
||||
r.Stroke()
|
||||
}
|
||||
}
|
||||
|
||||
// MeasureAnnotation measures how big an annotation would be.
|
||||
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
||||
style.WriteToRenderer(r)
|
||||
defer r.ResetStyle()
|
||||
|
||||
textBox := r.MeasureText(label)
|
||||
textWidth := textBox.Width()
|
||||
textHeight := textBox.Height()
|
||||
textWidth := int(textBox.Width())
|
||||
textHeight := int(textBox.Height())
|
||||
halfTextHeight := textHeight >> 1
|
||||
|
||||
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||
|
@ -203,8 +262,8 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
|
|||
defer r.ResetStyle()
|
||||
|
||||
textBox := r.MeasureText(label)
|
||||
textWidth := textBox.Width()
|
||||
halfTextHeight := textBox.Height() >> 1
|
||||
textWidth := int(textBox.Width())
|
||||
halfTextHeight := int(textBox.Height()) >> 1
|
||||
|
||||
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||
|
||||
|
@ -255,17 +314,17 @@ func (d draw) Box(r Renderer, b Box, s Style) {
|
|||
}
|
||||
|
||||
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
||||
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
|
||||
d.Box2d(r, b.Corners().Rotate(thetaDegrees), s)
|
||||
}
|
||||
|
||||
func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
|
||||
func (d draw) Box2d(r Renderer, bc Box2d, s Style) {
|
||||
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||
defer r.ResetStyle()
|
||||
|
||||
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
|
||||
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
|
||||
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
|
||||
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
|
||||
r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y))
|
||||
r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y))
|
||||
r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y))
|
||||
r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y))
|
||||
r.Close()
|
||||
r.FillStroke()
|
||||
}
|
||||
|
@ -278,7 +337,7 @@ func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
|||
r.Text(text, x, y)
|
||||
}
|
||||
|
||||
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
|
||||
func (d draw) MeasureText(r Renderer, text string, style Style) Box2d {
|
||||
style.GetTextOptions().WriteToRenderer(r)
|
||||
defer r.ResetStyle()
|
||||
|
||||
|
@ -297,9 +356,9 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
|||
|
||||
switch style.GetTextVerticalAlign() {
|
||||
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
||||
y = y - linesBox.Height()
|
||||
y = y - int(linesBox.Height())
|
||||
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
|
||||
y = (y - linesBox.Height()) >> 1
|
||||
y = (y - int(linesBox.Height())) >> 1
|
||||
}
|
||||
|
||||
var tx, ty int
|
||||
|
@ -307,19 +366,19 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
|||
lineBox := r.MeasureText(line)
|
||||
switch style.GetTextHorizontalAlign() {
|
||||
case TextHorizontalAlignCenter:
|
||||
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
||||
tx = box.Left + ((int(box.Width()) - int(lineBox.Width())) >> 1)
|
||||
case TextHorizontalAlignRight:
|
||||
tx = box.Right - lineBox.Width()
|
||||
tx = box.Right - int(lineBox.Width())
|
||||
default:
|
||||
tx = box.Left
|
||||
}
|
||||
if style.TextRotationDegrees == 0 {
|
||||
ty = y + lineBox.Height()
|
||||
ty = y + int(lineBox.Height())
|
||||
} else {
|
||||
ty = y
|
||||
}
|
||||
|
||||
r.Text(line, tx, ty)
|
||||
y += lineBox.Height() + style.GetTextLineSpacing()
|
||||
y += int(lineBox.Height()) + style.GetTextLineSpacing()
|
||||
}
|
||||
}
|
||||
|
|
53
drawing/color_test.go
Normal file
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
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
|
||||
|
@ -73,19 +66,6 @@ func (ema *EMASeries) GetValues(index int) (x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetFirstValues computes the first moving average value.
|
||||
func (ema *EMASeries) GetFirstValues() (x, y float64) {
|
||||
if ema.InnerSeries == nil {
|
||||
return
|
||||
}
|
||||
if len(ema.cache) == 0 {
|
||||
ema.ensureCachedValues()
|
||||
}
|
||||
x, _ = ema.InnerSeries.GetValues(0)
|
||||
y = ema.cache[0]
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValues computes the last moving average value but walking back window size samples,
|
||||
// and recomputing the last moving average chunk.
|
||||
func (ema *EMASeries) GetLastValues() (x, y float64) {
|
||||
|
|
106
ema_series_test.go
Normal file
106
ema_series_test.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
var (
|
||||
emaXValues = seq.Range(1.0, 50.0)
|
||||
emaYValues = []float64{
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
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 := mockValuesProvider{
|
||||
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.GetValues(x)
|
||||
yvalues = append(yvalues, y)
|
||||
}
|
||||
|
||||
for index, yv := range yvalues {
|
||||
assert.InDelta(yv, emaExpected[index], emaDelta)
|
||||
}
|
||||
|
||||
lvx, lvy := ema.GetLastValues()
|
||||
assert.Equal(50.0, lvx)
|
||||
assert.InDelta(lvy, emaExpected[49], emaDelta)
|
||||
}
|
|
@ -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},
|
||||
}
|
||||
}
|
4
font.go
4
font.go
|
@ -3,8 +3,8 @@ package chart
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/roboto"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/roboto"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
24
grid_line_test.go
Normal file
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)
|
||||
}
|
32
histogram_series_test.go
Normal file
32
histogram_series_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func TestHistogramSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: seq.Range(1.0, 20.0),
|
||||
YValues: seq.Range(10.0, -10.0),
|
||||
}
|
||||
|
||||
hs := HistogramSeries{
|
||||
InnerSeries: cs,
|
||||
}
|
||||
|
||||
for x := 0; x < hs.Len(); x++ {
|
||||
csx, csy := cs.GetValues(0)
|
||||
hsx, hsy1, hsy2 := hs.GetBoundedValues(0)
|
||||
assert.Equal(csx, hsx)
|
||||
assert.True(hsy1 > 0)
|
||||
assert.True(hsy2 <= 0)
|
||||
assert.True(csy < 0 || (csy > 0 && csy == hsy1))
|
||||
assert.True(csy > 0 || (csy < 0 && csy == hsy2))
|
||||
}
|
||||
}
|
2
jet.go
2
jet.go
|
@ -1,6 +1,6 @@
|
|||
package chart
|
||||
|
||||
import "git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
import "github.com/wcharczuk/go-chart/drawing"
|
||||
|
||||
// Jet is a color map provider based on matlab's jet color map.
|
||||
func Jet(v, vmin, vmax float64) drawing.Color {
|
||||
|
|
38
legend.go
38
legend.go
|
@ -1,8 +1,8 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"git.fireandbrimst.one/aw/go-chart/util"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// Legend returns a legend renderable function.
|
||||
|
@ -67,8 +67,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
if labelCount > 0 {
|
||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||
}
|
||||
legendContent.Bottom += tb.Height()
|
||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||
legendContent.Bottom += int(tb.Height())
|
||||
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||
labelCount++
|
||||
}
|
||||
|
@ -95,12 +95,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
tb := r.MeasureText(label)
|
||||
|
||||
ty := ycursor + tb.Height()
|
||||
ty := ycursor + int(tb.Height())
|
||||
r.Text(label, tx, ty)
|
||||
|
||||
th2 := tb.Height() >> 1
|
||||
th2 := int(tb.Height()) >> 1
|
||||
|
||||
lx := tx + tb.Width() + lineTextGap
|
||||
lx := tx + int(tb.Width()) + lineTextGap
|
||||
ly := ty - th2
|
||||
lx2 := legendContent.Right - legendPadding.Right
|
||||
|
||||
|
@ -112,7 +112,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx2, ly)
|
||||
r.Stroke()
|
||||
|
||||
ycursor += tb.Height()
|
||||
ycursor += int(tb.Height())
|
||||
legendCount++
|
||||
}
|
||||
}
|
||||
|
@ -160,12 +160,12 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
var textHeight int
|
||||
var textWidth int
|
||||
var textBox Box
|
||||
var textBox Box2d
|
||||
for x := 0; x < len(labels); x++ {
|
||||
if len(labels[x]) > 0 {
|
||||
textBox = r.MeasureText(labels[x])
|
||||
textHeight = util.Math.MaxInt(textBox.Height(), textHeight)
|
||||
textWidth = util.Math.MaxInt(textBox.Width(), textWidth)
|
||||
textHeight = util.Math.MaxInt(int(textBox.Height()), textHeight)
|
||||
textWidth = util.Math.MaxInt(int(textBox.Width()), textWidth)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
textBox = r.MeasureText(label)
|
||||
r.Text(label, tx, ty)
|
||||
|
||||
lx = tx + textBox.Width() + lineTextGap
|
||||
lx = tx + int(textBox.Width()) + lineTextGap
|
||||
ly = ty - th2
|
||||
|
||||
r.SetStrokeColor(lines[index].GetStrokeColor())
|
||||
|
@ -211,7 +211,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx+lineLengthMinimum, ly)
|
||||
r.Stroke()
|
||||
|
||||
tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
|
||||
tx += int(textBox.Width()) + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -279,8 +279,8 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
|||
if labelCount > 0 {
|
||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||
}
|
||||
legendContent.Bottom += tb.Height()
|
||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||
legendContent.Bottom += int(tb.Height())
|
||||
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||
labelCount++
|
||||
}
|
||||
|
@ -307,12 +307,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
tb := r.MeasureText(label)
|
||||
|
||||
ty := ycursor + tb.Height()
|
||||
ty := ycursor + int(tb.Height())
|
||||
r.Text(label, tx, ty)
|
||||
|
||||
th2 := tb.Height() >> 1
|
||||
th2 := int(tb.Height()) >> 1
|
||||
|
||||
lx := tx + tb.Width() + lineTextGap
|
||||
lx := tx + int(tb.Width()) + lineTextGap
|
||||
ly := ty - th2
|
||||
lx2 := legendContent.Right - legendPadding.Right
|
||||
|
||||
|
@ -324,7 +324,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx2, ly)
|
||||
r.Stroke()
|
||||
|
||||
ycursor += tb.Height()
|
||||
ycursor += int(tb.Height())
|
||||
legendCount++
|
||||
}
|
||||
}
|
||||
|
|
31
legend_test.go
Normal file
31
legend_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestLegend(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
graph := Chart{
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//note we have to do this as a separate step because we need a reference to graph
|
||||
graph.Elements = []Renderable{
|
||||
Legend(&graph),
|
||||
}
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := graph.Render(PNG, buf)
|
||||
assert.Nil(err)
|
||||
assert.NotZero(buf.Len())
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package chart
|
||||
|
||||
// LinearCoefficientProvider is a type that returns linear cofficients.
|
||||
type LinearCoefficientProvider interface {
|
||||
Coefficients() (m, b, stdev, avg float64)
|
||||
}
|
||||
|
||||
// LinearCoefficients returns a fixed linear coefficient pair.
|
||||
func LinearCoefficients(m, b float64) LinearCoefficientSet {
|
||||
return LinearCoefficientSet{
|
||||
M: m,
|
||||
B: b,
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizedLinearCoefficients returns a fixed linear coefficient pair.
|
||||
func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet {
|
||||
return LinearCoefficientSet{
|
||||
M: m,
|
||||
B: b,
|
||||
StdDev: stdev,
|
||||
Avg: avg,
|
||||
}
|
||||
}
|
||||
|
||||
// LinearCoefficientSet is the m and b values for the linear equation in the form:
|
||||
// y = (m*x) + b
|
||||
type LinearCoefficientSet struct {
|
||||
M float64
|
||||
B float64
|
||||
StdDev float64
|
||||
Avg float64
|
||||
}
|
||||
|
||||
// Coefficients returns the coefficients.
|
||||
func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) {
|
||||
m = lcs.M
|
||||
b = lcs.B
|
||||
stdev = lcs.StdDev
|
||||
avg = lcs.Avg
|
||||
return
|
||||
}
|
|
@ -3,16 +3,8 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/seq"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*LinearRegressionSeries)(nil)
|
||||
_ FirstValuesProvider = (*LinearRegressionSeries)(nil)
|
||||
_ LastValuesProvider = (*LinearRegressionSeries)(nil)
|
||||
_ LinearCoefficientProvider = (*LinearRegressionSeries)(nil)
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// LinearRegressionSeries is a series that plots the n-nearest neighbors
|
||||
|
@ -32,19 +24,6 @@ type LinearRegressionSeries struct {
|
|||
stddevx float64
|
||||
}
|
||||
|
||||
// Coefficients returns the linear coefficients for the series.
|
||||
func (lrs LinearRegressionSeries) Coefficients() (m, b, stdev, avg float64) {
|
||||
if lrs.IsZero() {
|
||||
lrs.computeCoefficients()
|
||||
}
|
||||
|
||||
m = lrs.m
|
||||
b = lrs.b
|
||||
stdev = lrs.stddevx
|
||||
avg = lrs.avgx
|
||||
return
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
func (lrs LinearRegressionSeries) GetName() string {
|
||||
return lrs.Name
|
||||
|
@ -93,7 +72,7 @@ func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) {
|
|||
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if lrs.IsZero() {
|
||||
if lrs.m == 0 && lrs.b == 0 {
|
||||
lrs.computeCoefficients()
|
||||
}
|
||||
offset := lrs.GetOffset()
|
||||
|
@ -103,25 +82,12 @@ func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetFirstValues computes the first linear regression value.
|
||||
func (lrs *LinearRegressionSeries) GetFirstValues() (x, y float64) {
|
||||
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if lrs.IsZero() {
|
||||
lrs.computeCoefficients()
|
||||
}
|
||||
x, y = lrs.InnerSeries.GetValues(0)
|
||||
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValues computes the last linear regression value.
|
||||
func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) {
|
||||
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if lrs.IsZero() {
|
||||
if lrs.m == 0 && lrs.b == 0 {
|
||||
lrs.computeCoefficients()
|
||||
}
|
||||
endIndex := lrs.GetEndIndex()
|
||||
|
@ -130,29 +96,6 @@ func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// Render renders the series.
|
||||
func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||
style := lrs.Style.InheritFrom(defaults)
|
||||
Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (lrs *LinearRegressionSeries) Validate() error {
|
||||
if lrs.InnerSeries == nil {
|
||||
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZero returns if we've computed the coefficients or not.
|
||||
func (lrs *LinearRegressionSeries) IsZero() bool {
|
||||
return lrs.m == 0 && lrs.b == 0
|
||||
}
|
||||
|
||||
//
|
||||
// internal helpers
|
||||
//
|
||||
|
||||
func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 {
|
||||
return (xvalue - lrs.avgx) / lrs.stddevx
|
||||
}
|
||||
|
@ -188,3 +131,17 @@ func (lrs *LinearRegressionSeries) computeCoefficients() {
|
|||
lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx)
|
||||
lrs.b = (sumy / p) - (lrs.m * sumx / p)
|
||||
}
|
||||
|
||||
// Render renders the series.
|
||||
func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||
style := lrs.Style.InheritFrom(defaults)
|
||||
Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (lrs *LinearRegressionSeries) Validate() error {
|
||||
if lrs.InnerSeries == nil {
|
||||
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
78
linear_regression_series_test.go
Normal file
78
linear_regression_series_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
)
|
||||
|
||||
func TestLinearRegressionSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(1.0, 100.0),
|
||||
YValues: seq.Range(1.0, 100.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
|
||||
lrx0, lry0 := linRegSeries.GetValues(0)
|
||||
assert.InDelta(1.0, lrx0, 0.0000001)
|
||||
assert.InDelta(1.0, lry0, 0.0000001)
|
||||
|
||||
lrxn, lryn := linRegSeries.GetLastValues()
|
||||
assert.InDelta(100.0, lrxn, 0.0000001)
|
||||
assert.InDelta(100.0, lryn, 0.0000001)
|
||||
}
|
||||
|
||||
func TestLinearRegressionSeriesDesc(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(100.0, 1.0),
|
||||
YValues: seq.Range(100.0, 1.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
InnerSeries: mainSeries,
|
||||
}
|
||||
|
||||
lrx0, lry0 := linRegSeries.GetValues(0)
|
||||
assert.InDelta(100.0, lrx0, 0.0000001)
|
||||
assert.InDelta(100.0, lry0, 0.0000001)
|
||||
|
||||
lrxn, lryn := linRegSeries.GetLastValues()
|
||||
assert.InDelta(1.0, lrxn, 0.0000001)
|
||||
assert.InDelta(1.0, lryn, 0.0000001)
|
||||
}
|
||||
|
||||
func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: seq.Range(100.0, 1.0),
|
||||
YValues: seq.Range(100.0, 1.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
InnerSeries: mainSeries,
|
||||
Offset: 10,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
assert.Equal(10, linRegSeries.Len())
|
||||
|
||||
lrx0, lry0 := linRegSeries.GetValues(0)
|
||||
assert.InDelta(90.0, lrx0, 0.0000001)
|
||||
assert.InDelta(90.0, lry0, 0.0000001)
|
||||
|
||||
lrxn, lryn := linRegSeries.GetLastValues()
|
||||
assert.InDelta(80.0, lrxn, 0.0000001)
|
||||
assert.InDelta(80.0, lryn, 0.0000001)
|
||||
}
|
119
linear_series.go
119
linear_series.go
|
@ -1,119 +0,0 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*LinearSeries)(nil)
|
||||
_ FirstValuesProvider = (*LinearSeries)(nil)
|
||||
_ LastValuesProvider = (*LinearSeries)(nil)
|
||||
)
|
||||
|
||||
// LinearSeries is a series that plots a line in a given domain.
|
||||
type LinearSeries struct {
|
||||
Name string
|
||||
Style Style
|
||||
YAxis YAxisType
|
||||
|
||||
XValues []float64
|
||||
InnerSeries LinearCoefficientProvider
|
||||
|
||||
m float64
|
||||
b float64
|
||||
stdev float64
|
||||
avg float64
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
func (ls LinearSeries) GetName() string {
|
||||
return ls.Name
|
||||
}
|
||||
|
||||
// GetStyle returns the line style.
|
||||
func (ls LinearSeries) GetStyle() Style {
|
||||
return ls.Style
|
||||
}
|
||||
|
||||
// GetYAxis returns which YAxis the series draws on.
|
||||
func (ls LinearSeries) GetYAxis() YAxisType {
|
||||
return ls.YAxis
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the series.
|
||||
func (ls LinearSeries) Len() int {
|
||||
return len(ls.XValues)
|
||||
}
|
||||
|
||||
// GetEndIndex returns the effective limit end.
|
||||
func (ls LinearSeries) GetEndIndex() int {
|
||||
return len(ls.XValues) - 1
|
||||
}
|
||||
|
||||
// GetValues gets a value at a given index.
|
||||
func (ls *LinearSeries) GetValues(index int) (x, y float64) {
|
||||
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||
return
|
||||
}
|
||||
if ls.IsZero() {
|
||||
ls.computeCoefficients()
|
||||
}
|
||||
x = ls.XValues[index]
|
||||
y = (ls.m * ls.normalize(x)) + ls.b
|
||||
return
|
||||
}
|
||||
|
||||
// GetFirstValues computes the first linear regression value.
|
||||
func (ls *LinearSeries) GetFirstValues() (x, y float64) {
|
||||
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||
return
|
||||
}
|
||||
if ls.IsZero() {
|
||||
ls.computeCoefficients()
|
||||
}
|
||||
x, y = ls.GetValues(0)
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValues computes the last linear regression value.
|
||||
func (ls *LinearSeries) GetLastValues() (x, y float64) {
|
||||
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||
return
|
||||
}
|
||||
if ls.IsZero() {
|
||||
ls.computeCoefficients()
|
||||
}
|
||||
x, y = ls.GetValues(ls.GetEndIndex())
|
||||
return
|
||||
}
|
||||
|
||||
// Render renders the series.
|
||||
func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||
Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls)
|
||||
}
|
||||
|
||||
// Validate validates the series.
|
||||
func (ls LinearSeries) Validate() error {
|
||||
if ls.InnerSeries == nil {
|
||||
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZero returns if the linear series has computed coefficients or not.
|
||||
func (ls LinearSeries) IsZero() bool {
|
||||
return ls.m == 0 && ls.b == 0
|
||||
}
|
||||
|
||||
// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
|
||||
func (ls *LinearSeries) computeCoefficients() {
|
||||
ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients()
|
||||
}
|
||||
|
||||
func (ls *LinearSeries) normalize(xvalue float64) float64 {
|
||||
if ls.avg > 0 && ls.stdev > 0 {
|
||||
return (xvalue - ls.avg) / ls.stdev
|
||||
}
|
||||
return xvalue
|
||||
}
|
88
macd_series_test.go
Normal file
88
macd_series_test.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
var (
|
||||
macdExpected = []float64{
|
||||
0,
|
||||
0.06381766382,
|
||||
0.1641441222,
|
||||
0.2817201894,
|
||||
0.4033023481,
|
||||
0.3924673744,
|
||||
0.2983093823,
|
||||
0.1561821464,
|
||||
-0.008916708129,
|
||||
-0.05210332292,
|
||||
-0.01649503993,
|
||||
0.06667130899,
|
||||
0.1751344574,
|
||||
0.1657328378,
|
||||
0.08257097469,
|
||||
-0.04265109369,
|
||||
-0.1875741257,
|
||||
-0.2091853882,
|
||||
-0.1518975486,
|
||||
-0.04781419838,
|
||||
0.08025242841,
|
||||
0.08881960494,
|
||||
0.02183529775,
|
||||
-0.08904155476,
|
||||
-0.2214141128,
|
||||
-0.2321805992,
|
||||
-0.1656331722,
|
||||
-0.05373789678,
|
||||
0.08083727586,
|
||||
0.09475354363,
|
||||
0.03209767112,
|
||||
-0.07534076818,
|
||||
-0.2050442354,
|
||||
-0.2138010557,
|
||||
-0.1458045181,
|
||||
-0.03293263556,
|
||||
0.1022243734,
|
||||
0.1163957964,
|
||||
0.05372761902,
|
||||
-0.05393941791,
|
||||
-0.1840438454,
|
||||
-0.1933365048,
|
||||
-0.1259788988,
|
||||
-0.01382225715,
|
||||
0.1205656194,
|
||||
0.1339326478,
|
||||
0.07044017167,
|
||||
-0.03805851969,
|
||||
-0.1689918111,
|
||||
-0.1791024416,
|
||||
}
|
||||
)
|
||||
|
||||
func TestMACDSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
mockSeries := mockValuesProvider{
|
||||
emaXValues,
|
||||
emaYValues,
|
||||
}
|
||||
assert.Equal(50, mockSeries.Len())
|
||||
|
||||
mas := &MACDSeries{
|
||||
InnerSeries: mockSeries,
|
||||
}
|
||||
|
||||
var yvalues []float64
|
||||
for x := 0; x < mas.Len(); x++ {
|
||||
_, y := mas.GetValues(x)
|
||||
yvalues = append(yvalues, y)
|
||||
}
|
||||
|
||||
assert.NotEmpty(yvalues)
|
||||
for index, vy := range yvalues {
|
||||
assert.InDelta(vy, macdExpected[index], emaDelta, fmt.Sprintf("delta @ %d actual: %0.9f expected: %0.9f", index, vy, macdExpected[index]))
|
||||
}
|
||||
}
|
194
market_hours_range.go
Normal file
194
market_hours_range.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wcharczuk/go-chart/seq"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// MarketHoursRange is a special type of range that compresses a time range into just the
|
||||
// market (i.e. NYSE operating hours and days) range.
|
||||
type MarketHoursRange struct {
|
||||
Min time.Time
|
||||
Max time.Time
|
||||
|
||||
MarketOpen time.Time
|
||||
MarketClose time.Time
|
||||
|
||||
HolidayProvider util.HolidayProvider
|
||||
|
||||
ValueFormatter ValueFormatter
|
||||
|
||||
Descending bool
|
||||
Domain int
|
||||
}
|
||||
|
||||
// IsDescending returns if the range is descending.
|
||||
func (mhr MarketHoursRange) IsDescending() bool {
|
||||
return mhr.Descending
|
||||
}
|
||||
|
||||
// GetTimezone returns the timezone for the market hours range.
|
||||
func (mhr MarketHoursRange) GetTimezone() *time.Location {
|
||||
return mhr.GetMarketOpen().Location()
|
||||
}
|
||||
|
||||
// IsZero returns if the range is setup or not.
|
||||
func (mhr MarketHoursRange) IsZero() bool {
|
||||
return mhr.Min.IsZero() && mhr.Max.IsZero()
|
||||
}
|
||||
|
||||
// GetMin returns the min value.
|
||||
func (mhr MarketHoursRange) GetMin() float64 {
|
||||
return util.Time.ToFloat64(mhr.Min)
|
||||
}
|
||||
|
||||
// GetMax returns the max value.
|
||||
func (mhr MarketHoursRange) GetMax() float64 {
|
||||
return util.Time.ToFloat64(mhr.GetEffectiveMax())
|
||||
}
|
||||
|
||||
// GetEffectiveMax gets either the close on the max, or the max itself.
|
||||
func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
|
||||
maxClose := util.Date.On(mhr.MarketClose, mhr.Max)
|
||||
if maxClose.After(mhr.Max) {
|
||||
return maxClose
|
||||
}
|
||||
return mhr.Max
|
||||
}
|
||||
|
||||
// SetMin sets the min value.
|
||||
func (mhr *MarketHoursRange) SetMin(min float64) {
|
||||
mhr.Min = util.Time.FromFloat64(min)
|
||||
mhr.Min = mhr.Min.In(mhr.GetTimezone())
|
||||
}
|
||||
|
||||
// SetMax sets the max value.
|
||||
func (mhr *MarketHoursRange) SetMax(max float64) {
|
||||
mhr.Max = util.Time.FromFloat64(max)
|
||||
mhr.Max = mhr.Max.In(mhr.GetTimezone())
|
||||
}
|
||||
|
||||
// GetDelta gets the delta.
|
||||
func (mhr MarketHoursRange) GetDelta() float64 {
|
||||
min := mhr.GetMin()
|
||||
max := mhr.GetMax()
|
||||
return max - min
|
||||
}
|
||||
|
||||
// GetDomain gets the domain.
|
||||
func (mhr MarketHoursRange) GetDomain() int {
|
||||
return mhr.Domain
|
||||
}
|
||||
|
||||
// SetDomain sets the domain.
|
||||
func (mhr *MarketHoursRange) SetDomain(domain int) {
|
||||
mhr.Domain = domain
|
||||
}
|
||||
|
||||
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
|
||||
func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider {
|
||||
if mhr.HolidayProvider == nil {
|
||||
return util.Date.IsNYSEHoliday
|
||||
}
|
||||
return mhr.HolidayProvider
|
||||
}
|
||||
|
||||
// GetMarketOpen returns the market open time.
|
||||
func (mhr MarketHoursRange) GetMarketOpen() time.Time {
|
||||
if mhr.MarketOpen.IsZero() {
|
||||
return util.NYSEOpen()
|
||||
}
|
||||
return mhr.MarketOpen
|
||||
}
|
||||
|
||||
// GetMarketClose returns the market close time.
|
||||
func (mhr MarketHoursRange) GetMarketClose() time.Time {
|
||||
if mhr.MarketClose.IsZero() {
|
||||
return util.NYSEClose()
|
||||
}
|
||||
return mhr.MarketClose
|
||||
}
|
||||
|
||||
// GetTicks returns the ticks for the range.
|
||||
// This is to override the default continous ticks that would be generated for the range.
|
||||
func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick {
|
||||
times := seq.TimeUtil.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
timesWidth := mhr.measureTimes(r, defaults, vf, times)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
times = seq.TimeUtil.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
times = seq.TimeUtil.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
times = seq.TimeUtil.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
times = seq.TimeUtil.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
|
||||
}
|
||||
|
||||
func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int {
|
||||
defaults.GetTextOptions().WriteToRenderer(r)
|
||||
var total int
|
||||
for index, t := range times {
|
||||
timeLabel := vf(t)
|
||||
|
||||
labelBox := r.MeasureText(timeLabel)
|
||||
total += int(labelBox.Width())
|
||||
if index > 0 {
|
||||
total += DefaultMinimumTickHorizontalSpacing
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []Tick {
|
||||
ticks := make([]Tick, len(times))
|
||||
for index, t := range times {
|
||||
ticks[index] = Tick{
|
||||
Value: util.Time.ToFloat64(t),
|
||||
Label: vf(t),
|
||||
}
|
||||
}
|
||||
return ticks
|
||||
}
|
||||
|
||||
func (mhr MarketHoursRange) String() string {
|
||||
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(time.RFC3339), mhr.Max.Format(time.RFC3339), mhr.Domain)
|
||||
}
|
||||
|
||||
// Translate maps a given value into the ContinuousRange space.
|
||||
func (mhr MarketHoursRange) Translate(value float64) int {
|
||||
valueTime := util.Time.FromFloat64(value)
|
||||
valueTimeEastern := valueTime.In(util.Date.Eastern())
|
||||
totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
|
||||
|
||||
if mhr.IsDescending() {
|
||||
return mhr.Domain - translated
|
||||
}
|
||||
|
||||
return translated
|
||||
}
|
73
market_hours_range_test.go
Normal file
73
market_hours_range_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func TestMarketHoursRangeGetDelta(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
r := &MarketHoursRange{
|
||||
Min: time.Date(2016, 07, 19, 9, 30, 0, 0, util.Date.Eastern()),
|
||||
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()),
|
||||
MarketOpen: util.NYSEOpen(),
|
||||
MarketClose: util.NYSEClose(),
|
||||
HolidayProvider: util.Date.IsNYSEHoliday,
|
||||
}
|
||||
|
||||
assert.NotZero(r.GetDelta())
|
||||
}
|
||||
|
||||
func TestMarketHoursRangeTranslate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
r := &MarketHoursRange{
|
||||
Min: time.Date(2016, 07, 18, 9, 30, 0, 0, util.Date.Eastern()),
|
||||
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()),
|
||||
MarketOpen: util.NYSEOpen(),
|
||||
MarketClose: util.NYSEClose(),
|
||||
HolidayProvider: util.Date.IsNYSEHoliday,
|
||||
Domain: 1000,
|
||||
}
|
||||
|
||||
weds := time.Date(2016, 07, 20, 9, 30, 0, 0, util.Date.Eastern())
|
||||
|
||||
assert.Equal(0, r.Translate(util.Time.ToFloat64(r.Min)))
|
||||
assert.Equal(400, r.Translate(util.Time.ToFloat64(weds)))
|
||||
assert.Equal(1000, r.Translate(util.Time.ToFloat64(r.Max)))
|
||||
}
|
||||
|
||||
func TestMarketHoursRangeGetTicks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
r, err := PNG(1024, 1024)
|
||||
assert.Nil(err)
|
||||
|
||||
f, err := GetDefaultFont()
|
||||
assert.Nil(err)
|
||||
|
||||
defaults := Style{
|
||||
Font: f,
|
||||
FontSize: 10,
|
||||
FontColor: ColorBlack,
|
||||
}
|
||||
|
||||
ra := &MarketHoursRange{
|
||||
Min: util.Date.On(util.NYSEOpen(), util.Date.Date(2016, 07, 18, util.Date.Eastern())),
|
||||
Max: util.Date.On(util.NYSEClose(), util.Date.Date(2016, 07, 22, util.Date.Eastern())),
|
||||
MarketOpen: util.NYSEOpen(),
|
||||
MarketClose: util.NYSEClose(),
|
||||
HolidayProvider: util.Date.IsNYSEHoliday,
|
||||
Domain: 1024,
|
||||
}
|
||||
|
||||
ticks := ra.GetTicks(r, defaults, TimeValueFormatter)
|
||||
assert.NotEmpty(ticks)
|
||||
assert.Len(ticks, 5)
|
||||
assert.NotEqual(util.Time.ToFloat64(ra.Min), ticks[0].Value)
|
||||
assert.NotEmpty(ticks[0].Label)
|
||||
}
|
396
matrix/matrix_test.go
Normal file
396
matrix/matrix_test.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
package matrix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := New(10, 5)
|
||||
rows, cols := m.Size()
|
||||
assert.Equal(10, rows)
|
||||
assert.Equal(5, cols)
|
||||
assert.Zero(m.Get(0, 0))
|
||||
assert.Zero(m.Get(9, 4))
|
||||
}
|
||||
|
||||
func TestNewWithValues(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := New(5, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
rows, cols := m.Size()
|
||||
assert.Equal(5, rows)
|
||||
assert.Equal(2, cols)
|
||||
assert.Equal(1, m.Get(0, 0))
|
||||
assert.Equal(10, m.Get(4, 1))
|
||||
}
|
||||
|
||||
func TestIdentitiy(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
id := Identity(5)
|
||||
rows, cols := id.Size()
|
||||
assert.Equal(5, rows)
|
||||
assert.Equal(5, cols)
|
||||
assert.Equal(1, id.Get(0, 0))
|
||||
assert.Equal(1, id.Get(1, 1))
|
||||
assert.Equal(1, id.Get(2, 2))
|
||||
assert.Equal(1, id.Get(3, 3))
|
||||
assert.Equal(1, id.Get(4, 4))
|
||||
assert.Equal(0, id.Get(0, 1))
|
||||
assert.Equal(0, id.Get(1, 0))
|
||||
assert.Equal(0, id.Get(4, 0))
|
||||
assert.Equal(0, id.Get(0, 4))
|
||||
}
|
||||
|
||||
func TestNewFromArrays(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3, 4},
|
||||
{5, 6, 7, 8},
|
||||
})
|
||||
assert.NotNil(m)
|
||||
|
||||
rows, cols := m.Size()
|
||||
assert.Equal(2, rows)
|
||||
assert.Equal(4, cols)
|
||||
}
|
||||
|
||||
func TestOnes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ones := Ones(5, 10)
|
||||
rows, cols := ones.Size()
|
||||
assert.Equal(5, rows)
|
||||
assert.Equal(10, cols)
|
||||
|
||||
for row := 0; row < rows; row++ {
|
||||
for col := 0; col < cols; col++ {
|
||||
assert.Equal(1, ones.Get(row, col))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatrixEpsilon(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ones := Ones(2, 2)
|
||||
ones = ones.WithEpsilon(0.001)
|
||||
assert.Equal(0.001, ones.Epsilon())
|
||||
}
|
||||
|
||||
func TestMatrixArrays(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
})
|
||||
|
||||
assert.NotNil(m)
|
||||
|
||||
arrays := m.Arrays()
|
||||
|
||||
assert.Equal(arrays, [][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatrixIsSquare(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.False(NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
}).IsSquare())
|
||||
|
||||
assert.False(NewFromArrays([][]float64{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
{5, 6},
|
||||
}).IsSquare())
|
||||
|
||||
assert.True(NewFromArrays([][]float64{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
}).IsSquare())
|
||||
}
|
||||
|
||||
func TestMatrixIsSymmetric(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.False(NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{2, 1, 2},
|
||||
}).IsSymmetric())
|
||||
|
||||
assert.False(NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
}).IsSymmetric())
|
||||
|
||||
assert.True(NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{2, 1, 2},
|
||||
{3, 2, 1},
|
||||
}).IsSymmetric())
|
||||
|
||||
}
|
||||
|
||||
func TestMatrixGet(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
assert.Equal(1, m.Get(0, 0))
|
||||
assert.Equal(2, m.Get(0, 1))
|
||||
assert.Equal(3, m.Get(0, 2))
|
||||
assert.Equal(4, m.Get(1, 0))
|
||||
assert.Equal(5, m.Get(1, 1))
|
||||
assert.Equal(6, m.Get(1, 2))
|
||||
assert.Equal(7, m.Get(2, 0))
|
||||
assert.Equal(8, m.Get(2, 1))
|
||||
assert.Equal(9, m.Get(2, 2))
|
||||
}
|
||||
|
||||
func TestMatrixSet(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
m.Set(1, 1, 99)
|
||||
assert.Equal(99, m.Get(1, 1))
|
||||
}
|
||||
|
||||
func TestMatrixCol(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
assert.Equal([]float64{1, 4, 7}, m.Col(0))
|
||||
assert.Equal([]float64{2, 5, 8}, m.Col(1))
|
||||
assert.Equal([]float64{3, 6, 9}, m.Col(2))
|
||||
}
|
||||
|
||||
func TestMatrixRow(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
assert.Equal([]float64{1, 2, 3}, m.Row(0))
|
||||
assert.Equal([]float64{4, 5, 6}, m.Row(1))
|
||||
assert.Equal([]float64{7, 8, 9}, m.Row(2))
|
||||
}
|
||||
|
||||
func TestMatrixSwapRows(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
m.SwapRows(0, 1)
|
||||
|
||||
assert.Equal([]float64{4, 5, 6}, m.Row(0))
|
||||
assert.Equal([]float64{1, 2, 3}, m.Row(1))
|
||||
assert.Equal([]float64{7, 8, 9}, m.Row(2))
|
||||
}
|
||||
|
||||
func TestMatrixCopy(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
m2 := m.Copy()
|
||||
assert.False(m == m2)
|
||||
assert.True(m.Equals(m2))
|
||||
}
|
||||
|
||||
func TestMatrixDiagonalVector(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 4, 7},
|
||||
{4, 2, 8},
|
||||
{7, 8, 3},
|
||||
})
|
||||
|
||||
diag := m.DiagonalVector()
|
||||
assert.Equal([]float64{1, 2, 3}, diag)
|
||||
}
|
||||
|
||||
func TestMatrixDiagonalVectorLandscape(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 4, 7, 99},
|
||||
{4, 2, 8, 99},
|
||||
})
|
||||
|
||||
diag := m.DiagonalVector()
|
||||
assert.Equal([]float64{1, 2}, diag)
|
||||
}
|
||||
|
||||
func TestMatrixDiagonalVectorPortrait(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 4},
|
||||
{4, 2},
|
||||
{99, 99},
|
||||
})
|
||||
|
||||
diag := m.DiagonalVector()
|
||||
assert.Equal([]float64{1, 2}, diag)
|
||||
}
|
||||
|
||||
func TestMatrixDiagonal(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 4, 7},
|
||||
{4, 2, 8},
|
||||
{7, 8, 3},
|
||||
})
|
||||
|
||||
m2 := NewFromArrays([][]float64{
|
||||
{1, 0, 0},
|
||||
{0, 2, 0},
|
||||
{0, 0, 3},
|
||||
})
|
||||
|
||||
assert.True(m.Diagonal().Equals(m2))
|
||||
}
|
||||
|
||||
func TestMatrixEquals(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 4, 7},
|
||||
{4, 2, 8},
|
||||
{7, 8, 3},
|
||||
})
|
||||
|
||||
assert.False(m.Equals(nil))
|
||||
var nilMatrix *Matrix
|
||||
assert.True(nilMatrix.Equals(nil))
|
||||
assert.False(m.Equals(New(1, 1)))
|
||||
assert.False(m.Equals(New(3, 3)))
|
||||
assert.True(m.Equals(New(3, 3, 1, 4, 7, 4, 2, 8, 7, 8, 3)))
|
||||
}
|
||||
|
||||
func TestMatrixL(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
l := m.L()
|
||||
assert.True(l.Equals(New(3, 3, 1, 2, 3, 0, 5, 6, 0, 0, 9)))
|
||||
}
|
||||
|
||||
func TestMatrixU(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
u := m.U()
|
||||
assert.True(u.Equals(New(3, 3, 0, 0, 0, 4, 0, 0, 7, 8, 0)))
|
||||
}
|
||||
|
||||
func TestMatrixString(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
})
|
||||
|
||||
assert.Equal("1 2 3 \n4 5 6 \n7 8 9 \n", m.String())
|
||||
}
|
||||
|
||||
func TestMatrixLU(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 3, 5},
|
||||
{2, 4, 7},
|
||||
{1, 1, 0},
|
||||
})
|
||||
|
||||
l, u, p := m.LU()
|
||||
assert.NotNil(l)
|
||||
assert.NotNil(u)
|
||||
assert.NotNil(p)
|
||||
}
|
||||
|
||||
func TestMatrixQR(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{12, -51, 4},
|
||||
{6, 167, -68},
|
||||
{-4, 24, -41},
|
||||
})
|
||||
|
||||
q, r := m.QR()
|
||||
assert.NotNil(q)
|
||||
assert.NotNil(r)
|
||||
}
|
||||
|
||||
func TestMatrixTranspose(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewFromArrays([][]float64{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
{10, 11, 12},
|
||||
})
|
||||
|
||||
m2 := m.Transpose()
|
||||
|
||||
rows, cols := m2.Size()
|
||||
assert.Equal(3, rows)
|
||||
assert.Equal(4, cols)
|
||||
|
||||
assert.Equal(1, m2.Get(0, 0))
|
||||
assert.Equal(10, m2.Get(0, 3))
|
||||
assert.Equal(3, m2.Get(2, 0))
|
||||
}
|
22
matrix/regression_test.go
Normal file
22
matrix/regression_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package matrix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestPoly(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
var xGiven = []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
var yGiven = []float64{1, 6, 17, 34, 57, 86, 121, 162, 209, 262, 321}
|
||||
var degree = 2
|
||||
|
||||
c, err := Poly(xGiven, yGiven, degree)
|
||||
assert.Nil(err)
|
||||
assert.Len(c, 3)
|
||||
|
||||
assert.InDelta(c[0], 0.999999999, DefaultEpsilon)
|
||||
assert.InDelta(c[1], 2, DefaultEpsilon)
|
||||
assert.InDelta(c[2], 3, DefaultEpsilon)
|
||||
}
|
41
pie_chart.go
41
pie_chart.go
|
@ -6,12 +6,11 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
const (
|
||||
_pi = math.Pi
|
||||
_pi2 = math.Pi / 2.0
|
||||
_pi4 = math.Pi / 4.0
|
||||
)
|
||||
|
@ -138,26 +137,19 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
|||
// draw the pie slices
|
||||
var rads, delta, delta2, total float64
|
||||
var lx, ly int
|
||||
for index, v := range values {
|
||||
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||
|
||||
if len(values) == 1 {
|
||||
pc.stylePieChartValue(0).WriteToRenderer(r)
|
||||
r.MoveTo(cx, cy)
|
||||
r.Circle(radius, cx, cy)
|
||||
} else {
|
||||
for index, v := range values {
|
||||
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||
rads = util.Math.PercentToRadians(total)
|
||||
delta = util.Math.PercentToRadians(v.Value)
|
||||
|
||||
r.MoveTo(cx, cy)
|
||||
rads = util.Math.PercentToRadians(total)
|
||||
delta = util.Math.PercentToRadians(v.Value)
|
||||
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||||
|
||||
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||||
|
||||
r.LineTo(cx, cy)
|
||||
r.Close()
|
||||
r.FillStroke()
|
||||
total = total + v.Value
|
||||
}
|
||||
r.LineTo(cx, cy)
|
||||
r.Close()
|
||||
r.FillStroke()
|
||||
total = total + v.Value
|
||||
}
|
||||
|
||||
// draw the labels
|
||||
|
@ -170,15 +162,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
|||
lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
|
||||
|
||||
tb := r.MeasureText(v.Label)
|
||||
lx = lx - (tb.Width() >> 1)
|
||||
ly = ly + (tb.Height() >> 1)
|
||||
|
||||
if lx < 0 {
|
||||
lx = 0
|
||||
}
|
||||
if ly < 0 {
|
||||
lx = 0
|
||||
}
|
||||
lx = lx - (int(tb.Width()) >> 1)
|
||||
ly = ly + (int(tb.Height()) >> 1)
|
||||
|
||||
r.Text(v.Label, lx, ly)
|
||||
}
|
||||
|
|
69
pie_chart_test.go
Normal file
69
pie_chart_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestPieChart(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
pie := PieChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 10, Label: "Blue"},
|
||||
{Value: 9, Label: "Green"},
|
||||
{Value: 8, Label: "Gray"},
|
||||
{Value: 7, Label: "Orange"},
|
||||
{Value: 6, Label: "HEANG"},
|
||||
{Value: 5, Label: "??"},
|
||||
{Value: 2, Label: "!!"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
pie.Render(PNG, b)
|
||||
assert.NotZero(b.Len())
|
||||
}
|
||||
|
||||
func TestPieChartDropsZeroValues(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
pie := PieChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 5, Label: "Blue"},
|
||||
{Value: 5, Label: "Green"},
|
||||
{Value: 0, Label: "Gray"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
err := pie.Render(PNG, b)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
func TestPieChartAllZeroValues(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
pie := PieChart{
|
||||
Canvas: Style{
|
||||
FillColor: ColorLightGray,
|
||||
},
|
||||
Values: []Value{
|
||||
{Value: 0, Label: "Blue"},
|
||||
{Value: 0, Label: "Green"},
|
||||
{Value: 0, Label: "Gray"},
|
||||
},
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer([]byte{})
|
||||
err := pie.Render(PNG, b)
|
||||
assert.NotNil(err)
|
||||
}
|
|
@ -4,15 +4,8 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/matrix"
|
||||
util "git.fireandbrimst.one/aw/go-chart/util"
|
||||
)
|
||||
|
||||
// Interface Assertions.
|
||||
var (
|
||||
_ Series = (*PolynomialRegressionSeries)(nil)
|
||||
_ FirstValuesProvider = (*PolynomialRegressionSeries)(nil)
|
||||
_ LastValuesProvider = (*PolynomialRegressionSeries)(nil)
|
||||
"github.com/wcharczuk/go-chart/matrix"
|
||||
util "github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// PolynomialRegressionSeries implements a polynomial regression over a given
|
||||
|
@ -108,23 +101,6 @@ func (prs *PolynomialRegressionSeries) GetValues(index int) (x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetFirstValues computes the first poly regression value.
|
||||
func (prs *PolynomialRegressionSeries) GetFirstValues() (x, y float64) {
|
||||
if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if prs.coeffs == nil {
|
||||
coeffs, err := prs.computeCoefficients()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prs.coeffs = coeffs
|
||||
}
|
||||
x, y = prs.InnerSeries.GetValues(0)
|
||||
y = prs.apply(x)
|
||||
return
|
||||
}
|
||||
|
||||
// GetLastValues computes the last poly regression value.
|
||||
func (prs *PolynomialRegressionSeries) GetLastValues() (x, y float64) {
|
||||
if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
|
||||
|
|
35
polynomial_regression_test.go
Normal file
35
polynomial_regression_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
"github.com/wcharczuk/go-chart/matrix"
|
||||
)
|
||||
|
||||
func TestPolynomialRegression(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var xv []float64
|
||||
var yv []float64
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
xv = append(xv, float64(i))
|
||||
yv = append(yv, float64(i*i))
|
||||
}
|
||||
|
||||
values := ContinuousSeries{
|
||||
XValues: xv,
|
||||
YValues: yv,
|
||||
}
|
||||
|
||||
poly := &PolynomialRegressionSeries{
|
||||
InnerSeries: values,
|
||||
Degree: 2,
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
_, y := poly.GetValues(i)
|
||||
assert.InDelta(float64(i*i), y, matrix.DefaultEpsilon)
|
||||
}
|
||||
}
|
|
@ -6,9 +6,9 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
|
||||
"git.fireandbrimst.one/aw/go-chart/drawing"
|
||||
"git.fireandbrimst.one/aw/go-chart/util"
|
||||
"git.fireandbrimst.one/aw/golang-freetype/truetype"
|
||||
util "github.com/blendlabs/go-util"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
// PNG returns a new png/raster renderer.
|
||||
|
@ -49,10 +49,6 @@ func (rr *rasterRenderer) SetDPI(dpi float64) {
|
|||
rr.gc.SetDPI(dpi)
|
||||
}
|
||||
|
||||
// SetClassName implements the interface method. However, PNGs have no classes.
|
||||
func (vr *rasterRenderer) SetClassName(_ string) {
|
||||
}
|
||||
|
||||
// SetStrokeColor implements the interface method.
|
||||
func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) {
|
||||
rr.s.StrokeColor = c
|
||||
|
@ -159,13 +155,13 @@ func (rr *rasterRenderer) Text(body string, x, y int) {
|
|||
}
|
||||
|
||||
// MeasureText returns the height and width in pixels of a string.
|
||||
func (rr *rasterRenderer) MeasureText(body string) Box {
|
||||
func (rr *rasterRenderer) MeasureText(body string) Box2d {
|
||||
rr.gc.SetFont(rr.s.Font)
|
||||
rr.gc.SetFontSize(rr.s.FontSize)
|
||||
rr.gc.SetFillColor(rr.s.FontColor)
|
||||
l, t, r, b, err := rr.gc.GetStringBounds(body)
|
||||
if err != nil {
|
||||
return Box{}
|
||||
return Box2d{}
|
||||
}
|
||||
if l < 0 {
|
||||
r = r - l // equivalent to r+(-1*l)
|
||||
|
@ -193,10 +189,10 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
|
|||
Bottom: int(math.Ceil(b)),
|
||||
}
|
||||
if rr.rotateRadians == nil {
|
||||
return textBox
|
||||
return textBox.Corners()
|
||||
}
|
||||
|
||||
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians)).Box()
|
||||
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians))
|
||||
}
|
||||
|
||||
// SetTextRotation sets a text rotation.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user