Compare commits

..

1 Commits

Author SHA1 Message Date
Will Charczuk
8721033046 checkin 2017-06-11 14:56:30 -07:00
135 changed files with 5418 additions and 1279 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

18
.gitignore vendored
View File

@ -1,19 +1 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Other
.vscode
.DS_Store
coverage.html

13
.travis.yml Normal file
View File

@ -0,0 +1,13 @@
language: go
go:
- 1.6.2
sudo: false
before_script:
- go get -u github.com/blendlabs/go-assert
- go get ./...
script:
- go test ./...

View File

@ -1 +0,0 @@
70.89

9
Makefile Normal file
View 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
View 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.

View File

@ -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) {

View File

@ -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{

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -8,7 +8,7 @@ import (
"strconv"
"time"
"git.fireandbrimst.one/aw/go-chart"
"github.com/wcharczuk/go-chart"
)
func random(min, max float64) float64 {

View File

@ -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))
}

View File

@ -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)

View File

@ -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{

View File

@ -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,

View File

@ -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) {

View File

@ -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

View File

@ -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))
}

View File

@ -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,

View File

@ -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,
},

View File

@ -4,7 +4,7 @@ import (
"fmt"
"log"
"git.fireandbrimst.one/aw/go-chart"
"github.com/wcharczuk/go-chart"
)
func main() {

View File

@ -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{

View File

@ -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{

View File

@ -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.

View 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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -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{

View File

@ -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))
}

View File

@ -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

View File

@ -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{

View File

@ -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,

View File

@ -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))
}

View File

@ -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{

View File

@ -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) {

View File

@ -5,21 +5,26 @@ 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(),
Title: "Test Stacked Bar Chart",
TitleStyle: chart.StyleShow(),
Orientation: chart.OrientationLandscape,
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.",

View File

@ -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,

View File

@ -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) {

View File

@ -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)

View File

@ -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{

View File

@ -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},
}

View File

@ -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
View 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)
}

View File

@ -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)))
@ -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())
}
}
@ -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
View 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)
}

View File

@ -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.

View 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))
}

2
box.go
View File

@ -4,7 +4,7 @@ import (
"fmt"
"math"
util "git.fireandbrimst.one/aw/go-chart/util"
util "github.com/wcharczuk/go-chart/util"
)
var (

188
box_test.go Normal file
View File

@ -0,0 +1,188 @@
package chart
import (
"math"
"testing"
"github.com/blendlabs/go-assert"
)
func TestBoxClone(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := a.Clone()
assert.True(a.Equals(b))
assert.True(b.Equals(a))
}
func TestBoxEquals(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := Box{Top: 10, Left: 10, Right: 30, Bottom: 30}
c := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
assert.True(a.Equals(a))
assert.True(a.Equals(c))
assert.True(c.Equals(a))
assert.False(a.Equals(b))
assert.False(c.Equals(b))
assert.False(b.Equals(a))
assert.False(b.Equals(c))
}
func TestBoxIsBiggerThan(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
assert.True(a.IsBiggerThan(b))
assert.False(a.IsBiggerThan(c))
assert.True(c.IsBiggerThan(a))
}
func TestBoxIsSmallerThan(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
assert.False(a.IsSmallerThan(b))
assert.True(a.IsSmallerThan(c))
assert.False(c.IsSmallerThan(a))
}
func TestBoxGrow(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 1, Left: 2, Right: 15, Bottom: 15}
b := Box{Top: 4, Left: 5, Right: 30, Bottom: 35}
c := a.Grow(b)
assert.False(c.Equals(b))
assert.False(c.Equals(a))
assert.Equal(1, c.Top)
assert.Equal(2, c.Left)
assert.Equal(30, c.Right)
assert.Equal(35, c.Bottom)
}
func TestBoxFit(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
fab := a.Fit(b)
assert.Equal(a.Left, fab.Left)
assert.Equal(a.Right, fab.Right)
assert.True(fab.Top < fab.Bottom)
assert.True(fab.Left < fab.Right)
assert.True(math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
fac := a.Fit(c)
assert.Equal(a.Top, fac.Top)
assert.Equal(a.Bottom, fac.Bottom)
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
}
func TestBoxConstrain(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
cab := a.Constrain(b)
assert.Equal(64, cab.Top)
assert.Equal(64, cab.Left)
assert.Equal(192, cab.Right)
assert.Equal(170, cab.Bottom)
cac := a.Constrain(c)
assert.Equal(64, cac.Top)
assert.Equal(64, cac.Left)
assert.Equal(170, cac.Right)
assert.Equal(192, cac.Bottom)
}
func TestBoxOuterConstrain(t *testing.T) {
assert := assert.New(t)
box := NewBox(0, 0, 100, 100)
canvas := NewBox(5, 5, 95, 95)
taller := NewBox(-10, 5, 50, 50)
c := canvas.OuterConstrain(box, taller)
assert.Equal(15, c.Top, c.String())
assert.Equal(5, c.Left, c.String())
assert.Equal(95, c.Right, c.String())
assert.Equal(95, c.Bottom, c.String())
wider := NewBox(5, 5, 110, 50)
d := canvas.OuterConstrain(box, wider)
assert.Equal(5, d.Top, d.String())
assert.Equal(5, d.Left, d.String())
assert.Equal(85, d.Right, d.String())
assert.Equal(95, d.Bottom, d.String())
}
func TestBoxShift(t *testing.T) {
assert := assert.New(t)
b := Box{
Top: 5,
Left: 5,
Right: 10,
Bottom: 10,
}
shifted := b.Shift(1, 2)
assert.Equal(7, shifted.Top)
assert.Equal(6, shifted.Left)
assert.Equal(11, shifted.Right)
assert.Equal(12, shifted.Bottom)
}
func TestBoxCenter(t *testing.T) {
assert := assert.New(t)
b := Box{
Top: 10,
Left: 10,
Right: 20,
Bottom: 30,
}
cx, cy := b.Center()
assert.Equal(15, cx)
assert.Equal(20, cy)
}
func TestBoxCornersCenter(t *testing.T) {
assert := assert.New(t)
bc := BoxCorners{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
cx, cy := bc.Center()
assert.Equal(10, cx)
assert.Equal(10, cy)
}
func TestBoxCornersRotate(t *testing.T) {
assert := assert.New(t)
bc := BoxCorners{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
rotated := bc.Rotate(45)
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
}

View File

@ -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

576
chart_test.go Normal file
View 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))
}

View File

@ -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
View 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
View 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))
}

View File

@ -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
View 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())
}

View File

@ -3,7 +3,7 @@ package chart
import (
"math"
util "git.fireandbrimst.one/aw/go-chart/util"
util "github.com/wcharczuk/go-chart/util"
)
var (

53
drawing/color_test.go Normal file
View 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
View 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())
}

View File

@ -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

View File

@ -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.

View File

@ -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, ...)

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
View 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)
}

View File

@ -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},
}
}

View File

@ -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
View 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
View 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
View File

@ -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 {

View File

@ -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.

31
legend_test.go Normal file
View 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())
}

View File

@ -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
}

View File

@ -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
}

View 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)
}

View File

@ -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
View 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]))
}
}

195
market_hours_range.go Normal file
View File

@ -0,0 +1,195 @@
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 func(_ time.Time) bool { return false }
}
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.Time.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.Time.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.Time.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.Time.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.Time.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 += 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.HolidayProvider)
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
if mhr.IsDescending() {
return mhr.Domain - translated
}
return translated
}

View 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
View 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
View 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)
}

View File

@ -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
@ -173,13 +165,6 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
lx = lx - (tb.Width() >> 1)
ly = ly + (tb.Height() >> 1)
if lx < 0 {
lx = 0
}
if ly < 0 {
lx = 0
}
r.Text(v.Label, lx, ly)
}
total = total + v.Value

69
pie_chart_test.go Normal file
View 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)
}

View File

@ -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 {

View 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)
}
}

View File

@ -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

View File

@ -3,8 +3,8 @@ package chart
import (
"io"
"git.fireandbrimst.one/aw/go-chart/drawing"
"git.fireandbrimst.one/aw/golang-freetype/truetype"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/drawing"
)
// Renderer represents the basic methods required to draw a chart.
@ -18,9 +18,6 @@ type Renderer interface {
// SetDPI sets the DPI for the renderer.
SetDPI(dpi float64)
// SetClassName sets the current class name.
SetClassName(string)
// SetStrokeColor sets the current stroke color.
SetStrokeColor(drawing.Color)

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
util "git.fireandbrimst.one/aw/go-chart/util"
util "github.com/wcharczuk/go-chart/util"
)
const (

192
seq/buffer_test.go Normal file
View File

@ -0,0 +1,192 @@
package seq
import (
"testing"
"github.com/blendlabs/go-assert"
)
func TestBuffer(t *testing.T) {
assert := assert.New(t)
buffer := NewBuffer()
buffer.Enqueue(1)
assert.Equal(1, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(1, buffer.PeekBack())
buffer.Enqueue(2)
assert.Equal(2, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(2, buffer.PeekBack())
buffer.Enqueue(3)
assert.Equal(3, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(3, buffer.PeekBack())
buffer.Enqueue(4)
assert.Equal(4, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(4, buffer.PeekBack())
buffer.Enqueue(5)
assert.Equal(5, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(5, buffer.PeekBack())
buffer.Enqueue(6)
assert.Equal(6, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(6, buffer.PeekBack())
buffer.Enqueue(7)
assert.Equal(7, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(7, buffer.PeekBack())
buffer.Enqueue(8)
assert.Equal(8, buffer.Len())
assert.Equal(1, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value := buffer.Dequeue()
assert.Equal(1, value)
assert.Equal(7, buffer.Len())
assert.Equal(2, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(2, value)
assert.Equal(6, buffer.Len())
assert.Equal(3, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(3, value)
assert.Equal(5, buffer.Len())
assert.Equal(4, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(4, value)
assert.Equal(4, buffer.Len())
assert.Equal(5, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(5, value)
assert.Equal(3, buffer.Len())
assert.Equal(6, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(6, value)
assert.Equal(2, buffer.Len())
assert.Equal(7, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(7, value)
assert.Equal(1, buffer.Len())
assert.Equal(8, buffer.Peek())
assert.Equal(8, buffer.PeekBack())
value = buffer.Dequeue()
assert.Equal(8, value)
assert.Equal(0, buffer.Len())
assert.Zero(buffer.Peek())
assert.Zero(buffer.PeekBack())
}
func TestBufferClear(t *testing.T) {
assert := assert.New(t)
buffer := NewBuffer()
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
buffer.Enqueue(1)
assert.Equal(8, buffer.Len())
buffer.Clear()
assert.Equal(0, buffer.Len())
assert.Zero(buffer.Peek())
assert.Zero(buffer.PeekBack())
}
func TestBufferArray(t *testing.T) {
assert := assert.New(t)
buffer := NewBuffer()
buffer.Enqueue(1)
buffer.Enqueue(2)
buffer.Enqueue(3)
buffer.Enqueue(4)
buffer.Enqueue(5)
contents := buffer.Array()
assert.Len(contents, 5)
assert.Equal(1, contents[0])
assert.Equal(2, contents[1])
assert.Equal(3, contents[2])
assert.Equal(4, contents[3])
assert.Equal(5, contents[4])
}
func TestBufferEach(t *testing.T) {
assert := assert.New(t)
buffer := NewBuffer()
for x := 1; x < 17; x++ {
buffer.Enqueue(float64(x))
}
called := 0
buffer.Each(func(_ int, v float64) {
if v == float64(called+1) {
called++
}
})
assert.Equal(16, called)
}
func TestNewBuffer(t *testing.T) {
assert := assert.New(t)
empty := NewBuffer()
assert.NotNil(empty)
assert.Zero(empty.Len())
assert.Equal(bufferDefaultCapacity, empty.Capacity())
assert.Zero(empty.Peek())
assert.Zero(empty.PeekBack())
}
func TestNewBufferWithValues(t *testing.T) {
assert := assert.New(t)
values := NewBuffer(1, 2, 3, 4, 5)
assert.NotNil(values)
assert.Equal(5, values.Len())
assert.Equal(1, values.Peek())
assert.Equal(5, values.PeekBack())
}
func TestBufferGrowth(t *testing.T) {
assert := assert.New(t)
values := NewBuffer(1, 2, 3, 4, 5)
for i := 0; i < 1<<10; i++ {
values.Enqueue(float64(i))
}
assert.Equal(1<<10-1, values.PeekBack())
}

48
seq/linear_test.go Normal file
View File

@ -0,0 +1,48 @@
package seq
import (
"testing"
assert "github.com/blendlabs/go-assert"
)
func TestRange(t *testing.T) {
assert := assert.New(t)
values := Range(1, 100)
assert.Len(values, 100)
assert.Equal(1, values[0])
assert.Equal(100, values[99])
}
func TestRangeWithStep(t *testing.T) {
assert := assert.New(t)
values := RangeWithStep(0, 100, 5)
assert.Equal(100, values[20])
assert.Len(values, 21)
}
func TestRangeReversed(t *testing.T) {
assert := assert.New(t)
values := Range(10.0, 1.0)
assert.Equal(10, len(values))
assert.Equal(10.0, values[0])
assert.Equal(1.0, values[9])
}
func TestValuesRegression(t *testing.T) {
assert := assert.New(t)
// note; this assumes a 1.0 step is implicitly set in the constructor.
linearProvider := NewLinear().WithStart(1.0).WithEnd(100.0)
assert.Equal(1, linearProvider.Start())
assert.Equal(100, linearProvider.End())
assert.Equal(100, linearProvider.Len())
values := Seq{Provider: linearProvider}.Array()
assert.Len(values, 100)
assert.Equal(1.0, values[0])
assert.Equal(100, values[99])
}

View File

@ -11,7 +11,7 @@ func RandomValues(count int) []float64 {
return Seq{NewRandom().WithLen(count)}.Array()
}
// RandomValuesWithMax returns an array of random values with a given average.
// RandomValuesWithAverage returns an array of random values with a given average.
func RandomValuesWithMax(count int, max float64) []float64 {
return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
}

20
seq/random_test.go Normal file
View File

@ -0,0 +1,20 @@
package seq
import (
"testing"
assert "github.com/blendlabs/go-assert"
)
func TestRandomRegression(t *testing.T) {
assert := assert.New(t)
randomProvider := NewRandom().WithLen(4096).WithMax(256)
assert.Equal(4096, randomProvider.Len())
assert.Equal(256, *randomProvider.Max())
randomSequence := New(randomProvider)
randomValues := randomSequence.Array()
assert.Len(randomValues, 4096)
assert.InDelta(128, randomSequence.Average(), 10.0)
}

Some files were not shown because too many files have changed in this diff Show More