Compare commits

..

12 Commits

Author SHA1 Message Date
Will Charczuk
de6df027fc updates 2017-05-17 16:01:23 -07:00
Will Charczuk
e3e851d2d1 ds store. 2017-05-17 12:44:24 -07:00
Will Charczuk
b537fd02cb bumping travis version. 2017-05-17 12:44:04 -07:00
Will Charczuk
5cf4f5f0d7 updates 2017-05-16 19:16:26 -07:00
Will Charczuk
04a4edcb46 stuff. 2017-05-16 17:50:17 -07:00
Will Charczuk
5936b89e89 updates 2017-05-16 13:31:36 -07:00
Will Charczuk
51f3cca5d7 need to fix market hours range tick size estimation 2017-05-14 18:58:10 -07:00
Will Charczuk
7ba2992824 wip 2017-05-14 16:33:48 -07:00
Will Charczuk
7d1401898a candle series, candle series tests. 2017-05-14 13:34:46 -07:00
Will Charczuk
e39acdfb76 time sequence stuff 2017-05-14 12:26:41 -07:00
Will Charczuk
566d798b32 time sequence stuff 2017-05-14 12:21:30 -07:00
Will Charczuk
73e3e439c5 helps to add the file. 2017-05-13 13:00:35 -07:00
146 changed files with 6477 additions and 1500 deletions

17
.gitignore vendored
View File

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

13
.travis.yml Normal file
View File

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

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

@ -0,0 +1,82 @@
package main
import (
"math/rand"
"net/http"
"time"
chart "github.com/wcharczuk/go-chart"
util "github.com/wcharczuk/go-chart/util"
)
func stockData() (times []time.Time, prices []float64) {
start := time.Date(2017, 05, 15, 9, 30, 0, 0, util.Date.Eastern())
price := 256.0
for day := 0; day < 60; day++ {
cursor := start.AddDate(0, 0, day)
if util.Date.IsNYSEHoliday(cursor) {
continue
}
for minute := 0; minute < ((6 * 60) + 30); minute++ {
cursor = cursor.Add(time.Minute)
if rand.Float64() >= 0.5 {
price = price + (rand.Float64() * (price * 0.01))
} else {
price = price - (rand.Float64() * (price * 0.01))
}
times = append(times, cursor)
prices = append(prices, price)
}
}
return
}
func drawChart(res http.ResponseWriter, req *http.Request) {
xv, yv := stockData()
priceSeries := chart.TimeSeries{
Name: "SPY",
Style: chart.Style{
Show: false,
StrokeColor: chart.GetDefaultColor(0),
},
XValues: xv,
YValues: yv,
}
candleSeries := chart.CandlestickSeries{
Name: "SPY",
XValues: xv,
YValues: yv,
}
graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{Show: true, FontSize: 8, TextRotationDegrees: 45},
TickPosition: chart.TickPositionUnderTick,
Range: &chart.MarketHoursRange{},
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
},
Series: []chart.Series{
candleSeries,
priceSeries,
},
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
if err != nil {
panic(err)
}
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

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{

49
_examples/overlap/main.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"net/http"
chart "github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
)
func conditionalColor(condition bool, trueColor drawing.Color, falseColor drawing.Color) drawing.Color {
if condition {
return trueColor
}
return falseColor
}
func drawChart(res http.ResponseWriter, req *http.Request) {
r, _ := chart.PNG(1024, 1024)
b0 := chart.Box{Left: 100, Top: 100, Right: 400, Bottom: 200}
b1 := chart.Box{Left: 500, Top: 100, Right: 900, Bottom: 200}
b0r := b0.Corners().Rotate(45).Shift(0, 200)
chart.Draw.Box(r, b0, chart.Style{
StrokeColor: drawing.ColorRed,
StrokeWidth: 2,
FillColor: conditionalColor(b0.Corners().Overlaps(b1.Corners()), drawing.ColorRed, drawing.ColorTransparent),
})
chart.Draw.Box(r, b1, chart.Style{
StrokeColor: drawing.ColorBlue,
StrokeWidth: 2,
FillColor: conditionalColor(b1.Corners().Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
})
chart.Draw.Box2d(r, b0r, chart.Style{
StrokeColor: drawing.ColorGreen,
StrokeWidth: 2,
FillColor: conditionalColor(b0r.Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
})
res.Header().Set("Content-Type", "image/png")
r.Save(res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

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,18 @@ import (
"log"
"net/http"
"git.fireandbrimst.one/aw/go-chart"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
sbc := chart.StackedBarChart{
Title: "Test Stacked Bar Chart",
TitleStyle: chart.StyleShow(),
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
XAxis: chart.StyleShow(),
YAxis: chart.StyleShow(),
XAxis: chart.Style{
Show: true,
},
YAxis: chart.Style{
Show: true,
},
Bars: []chart.StackedBar{
{
Name: "This is a very long string to test word break wrapping.",

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) {
@ -32,11 +32,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
tbc := tb.Corners().Rotate(45)
chart.Draw.BoxCorners(r, tbc, chart.Style{
chart.Draw.Box2d(r, tbc, chart.Style{
StrokeColor: drawing.ColorRed,
StrokeWidth: 2,
})
tbc2 := tbc.Shift(tbc.Height(), 0)
chart.Draw.Box2d(r, tbc2, chart.Style{
StrokeColor: drawing.ColorGreen,
StrokeWidth: 2,
})
tbcb := tbc.Box()
chart.Draw.Box(r, tbcb, chart.Style{
StrokeColor: drawing.ColorBlue,

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)))
@ -277,7 +261,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
r.Stroke()
var ty int
var tb Box
var tb Box2d
for _, t := range ticks {
ty = canvasBox.Bottom - yr.Translate(t.Value)
@ -288,7 +272,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
axisStyle.GetTextOptions().WriteToRenderer(r)
tb = r.MeasureText(t.Label)
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(int(tb.Height())>>1), axisStyle)
}
}
@ -296,32 +280,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
func (bc BarChart) drawTitle(r Renderer) {
if len(bc.Title) > 0 && bc.TitleStyle.Show {
r.SetFont(bc.TitleStyle.GetFont(bc.GetFont()))
r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor()))
titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize())
r.SetFontSize(titleFontSize)
textBox := r.MeasureText(bc.Title)
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (bc.GetWidth() >> 1) - (textWidth >> 1)
titleY := bc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
r.Text(bc.Title, titleX, titleY)
}
}
func (bc BarChart) getCanvasStyle() Style {
return bc.Canvas.InheritFrom(bc.styleDefaultsCanvas())
}
func (bc BarChart) styleDefaultsCanvas() Style {
return Style{
FillColor: bc.GetColorPalette().CanvasColor(),
StrokeColor: bc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultCanvasStrokeWidth,
Draw.TextWithin(r, bc.Title, bc.box(), bc.styleDefaultsTitle())
}
}
@ -410,7 +369,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
linesBox := Text.MeasureLines(r, lines, axisStyle)
xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
xaxisHeight = util.Math.MinInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight)
}
}
@ -438,8 +397,8 @@ func (bc BarChart) box() Box {
dpb := bc.Background.Padding.GetBottom(50)
return Box{
Top: bc.Background.Padding.GetTop(20),
Left: bc.Background.Padding.GetLeft(20),
Top: 20,
Left: 20,
Right: bc.GetWidth() - dpr,
Bottom: bc.GetHeight() - dpb,
}

320
bar_chart_test.go Normal file
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))
}

111
box.go
View File

@ -2,9 +2,8 @@ package chart
import (
"fmt"
"math"
util "git.fireandbrimst.one/aw/go-chart/util"
util "github.com/wcharczuk/go-chart/util"
)
var (
@ -166,12 +165,12 @@ func (b Box) Shift(x, y int) Box {
}
// Corners returns the box as a set of corners.
func (b Box) Corners() BoxCorners {
return BoxCorners{
TopLeft: Point{b.Left, b.Top},
TopRight: Point{b.Right, b.Top},
BottomRight: Point{b.Right, b.Bottom},
BottomLeft: Point{b.Left, b.Bottom},
func (b Box) Corners() Box2d {
return Box2d{
TopLeft: Point{float64(b.Left), float64(b.Top)},
TopRight: Point{float64(b.Right), float64(b.Top)},
BottomRight: Point{float64(b.Right), float64(b.Bottom)},
BottomLeft: Point{float64(b.Left), float64(b.Bottom)},
}
}
@ -255,99 +254,3 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
}
return newBox
}
// BoxCorners is a box with independent corners.
type BoxCorners struct {
TopLeft, TopRight, BottomRight, BottomLeft Point
}
// Box return the BoxCorners as a regular box.
func (bc BoxCorners) Box() Box {
return Box{
Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
}
}
// Width returns the width
func (bc BoxCorners) Width() int {
minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
return maxRight - minLeft
}
// Height returns the height
func (bc BoxCorners) Height() int {
minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
return maxBottom - minTop
}
// Center returns the center of the box
func (bc BoxCorners) Center() (x, y int) {
left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
x = ((right - left) >> 1) + left
top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
y = ((bottom - top) >> 1) + top
return
}
// Rotate rotates the box.
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
cx, cy := bc.Center()
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
return BoxCorners{
TopLeft: Point{tlx, tly},
TopRight: Point{trx, try},
BottomRight: Point{brx, bry},
BottomLeft: Point{blx, bly},
}
}
// Equals returns if the box equals another box.
func (bc BoxCorners) Equals(other BoxCorners) bool {
return bc.TopLeft.Equals(other.TopLeft) &&
bc.TopRight.Equals(other.TopRight) &&
bc.BottomRight.Equals(other.BottomRight) &&
bc.BottomLeft.Equals(other.BottomLeft)
}
func (bc BoxCorners) String() string {
return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
}
// Point is an X,Y pair
type Point struct {
X, Y int
}
// DistanceTo calculates the distance to another point.
func (p Point) DistanceTo(other Point) float64 {
dx := math.Pow(float64(p.X-other.X), 2)
dy := math.Pow(float64(p.Y-other.Y), 2)
return math.Pow(dx+dy, 0.5)
}
// Equals returns if a point equals another point.
func (p Point) Equals(other Point) bool {
return p.X == other.X && p.Y == other.Y
}
// String returns a string representation of the point.
func (p Point) String() string {
return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
}

183
box_2d.go Normal file
View File

@ -0,0 +1,183 @@
package chart
import (
"fmt"
"math"
util "github.com/wcharczuk/go-chart/util"
)
// Box2d is a box with (4) independent corners.
// It is used when dealing with ~rotated~ boxes.
type Box2d struct {
TopLeft, TopRight, BottomRight, BottomLeft Point
}
// Points returns the constituent points of the box.
func (bc Box2d) Points() []Point {
return []Point{
bc.TopRight,
bc.BottomRight,
bc.BottomLeft,
bc.TopLeft,
}
}
// Box return the Box2d as a regular box.
func (bc Box2d) Box() Box {
return Box{
Top: int(bc.Top()),
Left: int(bc.Left()),
Right: int(bc.Right()),
Bottom: int(bc.Bottom()),
}
}
// Top returns the top-most corner y value.
func (bc Box2d) Top() float64 {
return math.Min(bc.TopLeft.Y, bc.TopRight.Y)
}
// Left returns the left-most corner x value.
func (bc Box2d) Left() float64 {
return math.Min(bc.TopLeft.X, bc.BottomLeft.X)
}
// Right returns the right-most corner x value.
func (bc Box2d) Right() float64 {
return math.Max(bc.TopRight.X, bc.BottomRight.X)
}
// Bottom returns the bottom-most corner y value.
func (bc Box2d) Bottom() float64 {
return math.Max(bc.BottomLeft.Y, bc.BottomLeft.Y)
}
// Width returns the width
func (bc Box2d) Width() float64 {
minLeft := math.Min(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := math.Max(bc.TopRight.X, bc.BottomRight.X)
return maxRight - minLeft
}
// Height returns the height
func (bc Box2d) Height() float64 {
minTop := math.Min(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := math.Max(bc.BottomLeft.Y, bc.BottomRight.Y)
return maxBottom - minTop
}
// Center returns the center of the box
func (bc Box2d) Center() (x, y float64) {
left := util.Math.Mean(bc.TopLeft.X, bc.BottomLeft.X)
right := util.Math.Mean(bc.TopRight.X, bc.BottomRight.X)
x = ((right - left) / 2.0) + left
top := util.Math.Mean(bc.TopLeft.Y, bc.TopRight.Y)
bottom := util.Math.Mean(bc.BottomLeft.Y, bc.BottomRight.Y)
y = ((bottom - top) / 2.0) + top
return
}
// Rotate rotates the box.
func (bc Box2d) Rotate(thetaDegrees float64) Box2d {
cx, cy := bc.Center()
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
tlx, tly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopLeft.X), int(bc.TopLeft.Y), thetaRadians)
trx, try := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopRight.X), int(bc.TopRight.Y), thetaRadians)
brx, bry := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomRight.X), int(bc.BottomRight.Y), thetaRadians)
blx, bly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomLeft.X), int(bc.BottomLeft.Y), thetaRadians)
return Box2d{
TopLeft: Point{float64(tlx), float64(tly)},
TopRight: Point{float64(trx), float64(try)},
BottomRight: Point{float64(brx), float64(bry)},
BottomLeft: Point{float64(blx), float64(bly)},
}
}
// Shift shifts a box by a given x and y value.
func (bc Box2d) Shift(x, y float64) Box2d {
return Box2d{
TopLeft: bc.TopLeft.Shift(x, y),
TopRight: bc.TopRight.Shift(x, y),
BottomRight: bc.BottomRight.Shift(x, y),
BottomLeft: bc.BottomLeft.Shift(x, y),
}
}
// Equals returns if the box equals another box.
func (bc Box2d) Equals(other Box2d) bool {
return bc.TopLeft.Equals(other.TopLeft) &&
bc.TopRight.Equals(other.TopRight) &&
bc.BottomRight.Equals(other.BottomRight) &&
bc.BottomLeft.Equals(other.BottomLeft)
}
// Overlaps returns if two boxes overlap.
func (bc Box2d) Overlaps(other Box2d) bool {
pa := bc.Points()
pb := other.Points()
for i := 0; i < 4; i++ {
for j := 0; j < 4; j++ {
pa0 := pa[i]
pa1 := pa[(i+1)%4]
pb0 := pb[j]
pb1 := pb[(j+1)%4]
if util.Math.LinesIntersect(pa0.X, pa0.Y, pa1.X, pa1.Y, pb0.X, pb0.Y, pb1.X, pb1.Y) {
return true
}
}
}
return false
}
// Grow grows a box by a given set of dimensions.
func (bc Box2d) Grow(by Box) Box2d {
top, left, right, bottom := float64(by.Top), float64(by.Left), float64(by.Right), float64(by.Bottom)
return Box2d{
TopLeft: Point{X: bc.TopLeft.X - left, Y: bc.TopLeft.Y - top},
TopRight: Point{X: bc.TopRight.X + right, Y: bc.TopRight.Y - top},
BottomRight: Point{X: bc.BottomRight.X + right, Y: bc.BottomRight.Y + bottom},
BottomLeft: Point{X: bc.BottomLeft.X - left, Y: bc.BottomLeft.Y + bottom},
}
}
func (bc Box2d) String() string {
return fmt.Sprintf("Box2d{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
}
// Point is an X,Y pair
type Point struct {
X, Y float64
}
// Shift shifts a point.
func (p Point) Shift(x, y float64) Point {
return Point{
X: p.X + x,
Y: p.Y + y,
}
}
// DistanceTo calculates the distance to another point.
func (p Point) DistanceTo(other Point) float64 {
dx := math.Pow(p.X-other.X, 2)
dy := math.Pow(p.Y-other.Y, 2)
return math.Pow(dx+dy, 0.5)
}
// Equals returns if a point equals another point.
func (p Point) Equals(other Point) bool {
return p.X == other.X && p.Y == other.Y
}
// String returns a string representation of the point.
func (p Point) String() string {
return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y)
}

66
box_2d_test.go Normal file
View File

@ -0,0 +1,66 @@
package chart
import (
"fmt"
"testing"
assert "github.com/blendlabs/go-assert"
)
func TestBox2dCenter(t *testing.T) {
assert := assert.New(t)
bc := Box2d{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
cx, cy := bc.Center()
assert.Equal(10, cx)
assert.Equal(10, cy)
}
func TestBox2dRotate(t *testing.T) {
assert := assert.New(t)
bc := Box2d{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
rotated := bc.Rotate(45)
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
}
func TestBox2dOverlaps(t *testing.T) {
assert := assert.New(t)
bc := Box2d{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
// shift meaningfully the full width of bc right.
bc2 := bc.Shift(bc.Width()+1, 0)
assert.False(bc.Overlaps(bc2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc2))
// shift meaningfully the full height of bc down.
bc3 := bc.Shift(0, bc.Height()+1)
assert.False(bc.Overlaps(bc3), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc3))
bc4 := bc.Shift(5, 0)
assert.True(bc.Overlaps(bc4))
bc5 := bc.Shift(0, 5)
assert.True(bc.Overlaps(bc5))
bcr := bc.Rotate(45)
bcr2 := bc.Rotate(45).Shift(bc.Width()/2.0, 0)
assert.True(bcr.Overlaps(bcr2), fmt.Sprintf("%v\n\t\tshould overlap\n\t%v", bcr, bcr2))
}

159
box_test.go Normal file
View File

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

157
candlestick_series.go Normal file
View File

@ -0,0 +1,157 @@
package chart
import (
"fmt"
"time"
"math"
"github.com/wcharczuk/go-chart/util"
)
// CandleValue is a day's data for a candlestick plot.
type CandleValue struct {
Timestamp time.Time
High float64
Low float64
Open float64
Close float64
}
// String returns a string value for the candle value.
func (cv CandleValue) String() string {
return fmt.Sprintf("candle %s high: %.2f low: %.2f open: %.2f close: %.2f", cv.Timestamp.Format("2006-01-02"), cv.High, cv.Low, cv.Open, cv.Close)
}
// IsZero returns if the value is zero or not.
func (cv CandleValue) IsZero() bool {
return cv.Timestamp.IsZero()
}
// CandlestickSeries is a special type of series that takes a norma value provider
// and maps it to day value stats (high, low, open, close).
type CandlestickSeries struct {
Name string
Style Style
YAxis YAxisType
// CandleValues will be used in place of creating them from the `InnerSeries`.
CandleValues []CandleValue
// InnerSeries is used if the `CandleValues` are not set.
InnerSeries ValuesProvider
}
// GetName implements Series.GetName.
func (cs *CandlestickSeries) GetName() string {
return cs.Name
}
// GetStyle implements Series.GetStyle.
func (cs *CandlestickSeries) GetStyle() Style {
return cs.Style
}
// GetYAxis returns which yaxis the series is mapped to.
func (cs *CandlestickSeries) GetYAxis() YAxisType {
return cs.YAxis
}
// Len returns the length of the series.
func (cs *CandlestickSeries) Len() int {
return len(cs.GetCandleValues())
}
// GetBoundedValues returns the bounded values at a given index.
func (cs *CandlestickSeries) GetBoundedValues(index int) (x, y0, y1 float64) {
value := cs.GetCandleValues()[index]
return util.Time.ToFloat64(value.Timestamp), value.Low, value.High
}
// GetCandleValues returns the candle values.
func (cs CandlestickSeries) GetCandleValues() []CandleValue {
if cs.CandleValues == nil {
cs.CandleValues = cs.GenerateCandleValues()
}
return cs.CandleValues
}
// GenerateCandleValues returns the candlestick values for each day represented by the inner series.
func (cs CandlestickSeries) GenerateCandleValues() []CandleValue {
if cs.InnerSeries == nil {
return nil
}
totalValues := cs.InnerSeries.Len()
if totalValues == 0 {
return nil
}
var values []CandleValue
var lastYear, lastMonth, lastDay int
var year, month, day int
var t time.Time
var tv, lv, v float64
tv, v = cs.InnerSeries.GetValues(0)
t = util.Time.FromFloat64(tv)
year, month, day = t.Year(), int(t.Month()), t.Day()
lastYear, lastMonth, lastDay = year, month, day
value := CandleValue{
Timestamp: cs.newTimestamp(year, month, day),
Open: v,
Low: v,
High: v,
}
lv = v
for i := 1; i < totalValues; i++ {
tv, v = cs.InnerSeries.GetValues(i)
t = util.Time.FromFloat64(tv)
year, month, day = t.Year(), int(t.Month()), t.Day()
// if we've transitioned to a new day or we're on the last value
if lastYear != year || lastMonth != month || lastDay != day || i == (totalValues-1) {
value.Close = lv
values = append(values, value)
value = CandleValue{
Timestamp: cs.newTimestamp(year, month, day),
Open: v,
High: v,
Low: v,
}
lastYear = year
lastMonth = month
lastDay = day
} else {
value.Low = math.Min(value.Low, v)
value.High = math.Max(value.High, v)
}
lv = v
}
return values
}
func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time {
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, util.Date.Eastern())
}
// Render implements Series.Render.
func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := cs.Style.InheritFrom(defaults)
Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs)
}
// Validate validates the series.
func (cs CandlestickSeries) Validate() error {
if cs.CandleValues == nil && cs.InnerSeries == nil {
return fmt.Errorf("candlestick series requires either `CandleValues` or `InnerSeries` to be set")
}
return nil
}

View File

@ -0,0 +1,52 @@
package chart
import (
"math/rand"
"testing"
"time"
assert "github.com/blendlabs/go-assert"
"github.com/wcharczuk/go-chart/util"
)
func generateDummyStockData() (times []time.Time, prices []float64) {
start := util.Date.On(util.NYSEOpen(), time.Date(2017, 05, 15, 0, 0, 0, 0, util.Date.Eastern()))
cursor := start
for day := 0; day < 60; day++ {
if util.Date.IsWeekendDay(cursor.Weekday()) {
cursor = start.AddDate(0, 0, day)
continue
}
for hour := 0; hour < 7; hour++ {
for minute := 0; minute < 60; minute++ {
times = append(times, cursor)
prices = append(prices, rand.Float64()*256)
cursor = cursor.Add(time.Minute)
}
cursor = cursor.Add(time.Hour)
}
cursor = start.AddDate(0, 0, day)
}
return
}
func TestCandlestickSeriesCandleValues(t *testing.T) {
assert := assert.New(t)
xdata, ydata := generateDummyStockData()
candleSeries := &CandlestickSeries{
InnerSeries: TimeSeries{
XValues: xdata,
YValues: ydata,
},
}
values := candleSeries.GetCandleValues()
assert.Len(values, 43) // should be 60 days per the generator.
}

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
@ -496,8 +502,8 @@ func (c Chart) drawTitle(r Renderer) {
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
titleX := (int(c.GetWidth()) >> 1) - (int(textWidth) >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(textHeight)
r.Text(c.Title, titleX, titleY)
}

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

BIN
debug.test Executable file

Binary file not shown.

95
draw.go
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 (
@ -168,14 +168,73 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
}
}
func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, cs CandlestickSeries) {
if cs.Len() == 0 {
return
}
candleValues := cs.GetCandleValues()
cb := canvasBox.Bottom
cl := canvasBox.Left
var cv CandleValue
for index := 0; index < len(candleValues); index++ {
cv = candleValues[index]
y0 := yrange.Translate(cv.Open)
y1 := yrange.Translate(cv.Close)
x0 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEOpen(), cv.Timestamp)))
x1 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEClose(), cv.Timestamp)))
x := x0 + ((x1 - x0) >> 1)
// draw open / close box.
if cv.Open < cv.Close {
d.Box(r, Box{
Top: cb - y0,
Left: x0,
Right: x1,
Bottom: cb - y1,
}, style.InheritFrom(Style{FillColor: ColorAlternateGreen}))
} else {
d.Box(r, Box{
Top: cb - y1,
Left: x0,
Right: x1,
Bottom: cb - y0,
}, style.InheritFrom(Style{FillColor: ColorRed}))
}
// draw high / low t bars
y0 = yrange.Translate(cv.High)
y1 = yrange.Translate(cv.Low)
style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r)
r.MoveTo(x0, cb-y0)
r.LineTo(x1, cb-y0)
r.Stroke()
r.MoveTo(x, cb-y0)
r.LineTo(x, cb-y1)
r.Stroke()
r.MoveTo(x0, cb-y1)
r.LineTo(x1, cb-y1)
r.Stroke()
}
}
// MeasureAnnotation measures how big an annotation would be.
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
style.WriteToRenderer(r)
defer r.ResetStyle()
textBox := r.MeasureText(label)
textWidth := textBox.Width()
textHeight := textBox.Height()
textWidth := int(textBox.Width())
textHeight := int(textBox.Height())
halfTextHeight := textHeight >> 1
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
@ -203,8 +262,8 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
defer r.ResetStyle()
textBox := r.MeasureText(label)
textWidth := textBox.Width()
halfTextHeight := textBox.Height() >> 1
textWidth := int(textBox.Width())
halfTextHeight := int(textBox.Height()) >> 1
style.GetFillAndStrokeOptions().WriteToRenderer(r)
@ -255,17 +314,17 @@ func (d draw) Box(r Renderer, b Box, s Style) {
}
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
d.Box2d(r, b.Corners().Rotate(thetaDegrees), s)
}
func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
func (d draw) Box2d(r Renderer, bc Box2d, s Style) {
s.GetFillAndStrokeOptions().WriteToRenderer(r)
defer r.ResetStyle()
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y))
r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y))
r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y))
r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y))
r.Close()
r.FillStroke()
}
@ -278,7 +337,7 @@ func (d draw) Text(r Renderer, text string, x, y int, style Style) {
r.Text(text, x, y)
}
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
func (d draw) MeasureText(r Renderer, text string, style Style) Box2d {
style.GetTextOptions().WriteToRenderer(r)
defer r.ResetStyle()
@ -297,9 +356,9 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
switch style.GetTextVerticalAlign() {
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
y = y - linesBox.Height()
y = y - int(linesBox.Height())
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
y = (y - linesBox.Height()) >> 1
y = (y - int(linesBox.Height())) >> 1
}
var tx, ty int
@ -307,19 +366,19 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
lineBox := r.MeasureText(line)
switch style.GetTextHorizontalAlign() {
case TextHorizontalAlignCenter:
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
tx = box.Left + ((int(box.Width()) - int(lineBox.Width())) >> 1)
case TextHorizontalAlignRight:
tx = box.Right - lineBox.Width()
tx = box.Right - int(lineBox.Width())
default:
tx = box.Left
}
if style.TextRotationDegrees == 0 {
ty = y + lineBox.Height()
ty = y + int(lineBox.Height())
} else {
ty = y
}
r.Text(line, tx, ty)
y += lineBox.Height() + style.GetTextLineSpacing()
y += int(lineBox.Height()) + style.GetTextLineSpacing()
}
}

53
drawing/color_test.go Normal file
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.
@ -67,8 +67,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
if labelCount > 0 {
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
}
legendContent.Bottom += tb.Height()
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
legendContent.Bottom += int(tb.Height())
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
labelCount++
}
@ -95,12 +95,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
tb := r.MeasureText(label)
ty := ycursor + tb.Height()
ty := ycursor + int(tb.Height())
r.Text(label, tx, ty)
th2 := tb.Height() >> 1
th2 := int(tb.Height()) >> 1
lx := tx + tb.Width() + lineTextGap
lx := tx + int(tb.Width()) + lineTextGap
ly := ty - th2
lx2 := legendContent.Right - legendPadding.Right
@ -112,7 +112,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
r.LineTo(lx2, ly)
r.Stroke()
ycursor += tb.Height()
ycursor += int(tb.Height())
legendCount++
}
}
@ -160,12 +160,12 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
var textHeight int
var textWidth int
var textBox Box
var textBox Box2d
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
textBox = r.MeasureText(labels[x])
textHeight = util.Math.MaxInt(textBox.Height(), textHeight)
textWidth = util.Math.MaxInt(textBox.Width(), textWidth)
textHeight = util.Math.MaxInt(int(textBox.Height()), textHeight)
textWidth = util.Math.MaxInt(int(textBox.Width()), textWidth)
}
}
@ -200,7 +200,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
textBox = r.MeasureText(label)
r.Text(label, tx, ty)
lx = tx + textBox.Width() + lineTextGap
lx = tx + int(textBox.Width()) + lineTextGap
ly = ty - th2
r.SetStrokeColor(lines[index].GetStrokeColor())
@ -211,7 +211,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
r.LineTo(lx+lineLengthMinimum, ly)
r.Stroke()
tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
tx += int(textBox.Width()) + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
}
}
}
@ -279,8 +279,8 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
if labelCount > 0 {
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
}
legendContent.Bottom += tb.Height()
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
legendContent.Bottom += int(tb.Height())
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
labelCount++
}
@ -307,12 +307,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
tb := r.MeasureText(label)
ty := ycursor + tb.Height()
ty := ycursor + int(tb.Height())
r.Text(label, tx, ty)
th2 := tb.Height() >> 1
th2 := int(tb.Height()) >> 1
lx := tx + tb.Width() + lineTextGap
lx := tx + int(tb.Width()) + lineTextGap
ly := ty - th2
lx2 := legendContent.Right - legendPadding.Right
@ -324,7 +324,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
r.LineTo(lx2, ly)
r.Stroke()
ycursor += tb.Height()
ycursor += int(tb.Height())
legendCount++
}
}

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

194
market_hours_range.go Normal file
View File

@ -0,0 +1,194 @@
package chart
import (
"fmt"
"time"
"github.com/wcharczuk/go-chart/seq"
"github.com/wcharczuk/go-chart/util"
)
// MarketHoursRange is a special type of range that compresses a time range into just the
// market (i.e. NYSE operating hours and days) range.
type MarketHoursRange struct {
Min time.Time
Max time.Time
MarketOpen time.Time
MarketClose time.Time
HolidayProvider util.HolidayProvider
ValueFormatter ValueFormatter
Descending bool
Domain int
}
// IsDescending returns if the range is descending.
func (mhr MarketHoursRange) IsDescending() bool {
return mhr.Descending
}
// GetTimezone returns the timezone for the market hours range.
func (mhr MarketHoursRange) GetTimezone() *time.Location {
return mhr.GetMarketOpen().Location()
}
// IsZero returns if the range is setup or not.
func (mhr MarketHoursRange) IsZero() bool {
return mhr.Min.IsZero() && mhr.Max.IsZero()
}
// GetMin returns the min value.
func (mhr MarketHoursRange) GetMin() float64 {
return util.Time.ToFloat64(mhr.Min)
}
// GetMax returns the max value.
func (mhr MarketHoursRange) GetMax() float64 {
return util.Time.ToFloat64(mhr.GetEffectiveMax())
}
// GetEffectiveMax gets either the close on the max, or the max itself.
func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
maxClose := util.Date.On(mhr.MarketClose, mhr.Max)
if maxClose.After(mhr.Max) {
return maxClose
}
return mhr.Max
}
// SetMin sets the min value.
func (mhr *MarketHoursRange) SetMin(min float64) {
mhr.Min = util.Time.FromFloat64(min)
mhr.Min = mhr.Min.In(mhr.GetTimezone())
}
// SetMax sets the max value.
func (mhr *MarketHoursRange) SetMax(max float64) {
mhr.Max = util.Time.FromFloat64(max)
mhr.Max = mhr.Max.In(mhr.GetTimezone())
}
// GetDelta gets the delta.
func (mhr MarketHoursRange) GetDelta() float64 {
min := mhr.GetMin()
max := mhr.GetMax()
return max - min
}
// GetDomain gets the domain.
func (mhr MarketHoursRange) GetDomain() int {
return mhr.Domain
}
// SetDomain sets the domain.
func (mhr *MarketHoursRange) SetDomain(domain int) {
mhr.Domain = domain
}
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider {
if mhr.HolidayProvider == nil {
return util.Date.IsNYSEHoliday
}
return mhr.HolidayProvider
}
// GetMarketOpen returns the market open time.
func (mhr MarketHoursRange) GetMarketOpen() time.Time {
if mhr.MarketOpen.IsZero() {
return util.NYSEOpen()
}
return mhr.MarketOpen
}
// GetMarketClose returns the market close time.
func (mhr MarketHoursRange) GetMarketClose() time.Time {
if mhr.MarketClose.IsZero() {
return util.NYSEClose()
}
return mhr.MarketClose
}
// GetTicks returns the ticks for the range.
// This is to override the default continous ticks that would be generated for the range.
func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick {
times := seq.TimeUtil.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
timesWidth := mhr.measureTimes(r, defaults, vf, times)
if timesWidth <= mhr.Domain {
return mhr.makeTicks(vf, times)
}
times = seq.TimeUtil.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
timesWidth = mhr.measureTimes(r, defaults, vf, times)
if timesWidth <= mhr.Domain {
return mhr.makeTicks(vf, times)
}
times = seq.TimeUtil.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
timesWidth = mhr.measureTimes(r, defaults, vf, times)
if timesWidth <= mhr.Domain {
return mhr.makeTicks(vf, times)
}
times = seq.TimeUtil.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
timesWidth = mhr.measureTimes(r, defaults, vf, times)
if timesWidth <= mhr.Domain {
return mhr.makeTicks(vf, times)
}
times = seq.TimeUtil.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
timesWidth = mhr.measureTimes(r, defaults, vf, times)
if timesWidth <= mhr.Domain {
return mhr.makeTicks(vf, times)
}
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
}
func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int {
defaults.GetTextOptions().WriteToRenderer(r)
var total int
for index, t := range times {
timeLabel := vf(t)
labelBox := r.MeasureText(timeLabel)
total += int(labelBox.Width())
if index > 0 {
total += DefaultMinimumTickHorizontalSpacing
}
}
return total
}
func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []Tick {
ticks := make([]Tick, len(times))
for index, t := range times {
ticks[index] = Tick{
Value: util.Time.ToFloat64(t),
Label: vf(t),
}
}
return ticks
}
func (mhr MarketHoursRange) String() string {
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(time.RFC3339), mhr.Max.Format(time.RFC3339), mhr.Domain)
}
// Translate maps a given value into the ContinuousRange space.
func (mhr MarketHoursRange) Translate(value float64) int {
valueTime := util.Time.FromFloat64(value)
valueTimeEastern := valueTime.In(util.Date.Eastern())
totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
if mhr.IsDescending() {
return mhr.Domain - translated
}
return translated
}

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
@ -170,15 +162,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
ly = ly + (tb.Height() >> 1)
if lx < 0 {
lx = 0
}
if ly < 0 {
lx = 0
}
lx = lx - (int(tb.Width()) >> 1)
ly = ly + (int(tb.Height()) >> 1)
r.Text(v.Label, lx, ly)
}

69
pie_chart_test.go Normal file
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
@ -159,13 +155,13 @@ func (rr *rasterRenderer) Text(body string, x, y int) {
}
// MeasureText returns the height and width in pixels of a string.
func (rr *rasterRenderer) MeasureText(body string) Box {
func (rr *rasterRenderer) MeasureText(body string) Box2d {
rr.gc.SetFont(rr.s.Font)
rr.gc.SetFontSize(rr.s.FontSize)
rr.gc.SetFillColor(rr.s.FontColor)
l, t, r, b, err := rr.gc.GetStringBounds(body)
if err != nil {
return Box{}
return Box2d{}
}
if l < 0 {
r = r - l // equivalent to r+(-1*l)
@ -193,10 +189,10 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
Bottom: int(math.Ceil(b)),
}
if rr.rotateRadians == nil {
return textBox
return textBox.Corners()
}
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians)).Box()
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians))
}
// SetTextRotation sets a text rotation.

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