Compare commits

...

74 commits

Author SHA1 Message Date
63f0be79dd add web server sample 2024-10-27 22:48:01 -05:00
1f6d76e8b5 fix paths 2024-10-27 22:24:27 -05:00
cc8d36edd9 update url
Some checks failed
Continuous Integration / Tests (push) Has been cancelled
2024-10-27 21:52:38 -05:00
bd2b44aa38 Start copy from https://github.com/wcharczuk/go-chart 2024-10-27 16:38:55 -05:00
Will Charczuk
b46667ea80
Update README.md (#230) 2024-08-23 08:50:22 -07:00
Will Charczuk
218e744a87 narrow fix for CVE-2024-40060 2024-08-23 08:41:44 -07:00
dependabot[bot]
a334e8e43a
Bump golang.org/x/image from 0.12.0 to 0.18.0 (#225)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.12.0 to 0.18.0.
- [Commits](https://github.com/golang/image/compare/v0.12.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 12:37:07 -07:00
Will Charczuk
c9c9042154 add transparent 2023-11-04 15:44:23 -07:00
Will Charczuk
2f3402adfb getting ready to add extended colors but stopping at basic colors for now 2023-11-04 13:30:06 -07:00
Will Charczuk
dd823617d9 color from hex handles # now 2023-11-04 09:44:19 -07:00
Will Charczuk
7281bbdfa8 tweaking how svg renders itself 2023-10-03 08:43:15 -07:00
Will Charczuk
b3fc6cba9f make it not impossible to set a transparent background color 2023-10-03 08:23:15 -07:00
Will Charczuk
0d3588f719 updates to readme 2023-09-11 16:19:37 -04:00
Will Charczuk
d9bba03c8f status badge 2023-09-11 16:17:58 -04:00
Will Charczuk
c6416e0757 updates to ci, fixing busts in examples 2023-09-11 16:10:03 -04:00
Will Charczuk
f6d9d1edca fixing some issues with examples 2023-09-11 12:27:27 -04:00
Will Charczuk
2c2fb0651b removing go-sdk 2023-09-11 12:21:40 -04:00
Will Charczuk
2ff54048b8
Update README.md (#212) 2023-07-13 10:08:18 -07:00
guangwu
e781e0cd22
chore: unnecessary use of fmt.Sprintf (#211) 2023-07-10 08:53:10 -07:00
Thomas Lambert
54fc699377
fix(bar_charts): use YAxis.Render (#209) 2023-06-12 16:04:52 +02:00
twessling-icas
1ccfbb0172
add logarithmic axes support, with tests. Supports positive Y-values only. (#141)
Co-authored-by: Ton Wessling <twessling@ebay.com>
2023-05-22 08:37:57 -07:00
Will Charczuk
c1468e8ae4
Adds support for go mod (finally) (#164) 2020-11-22 16:45:10 -08:00
Jamie Isaacs
962b9abdec Add stacked bar chart value labels (#60) and a horizontal stacked bar chart (#39) (#114)
* Add stacked bar chart value labels (#60)

* Add horizontal render option to stacked bar chart (#39)

* Pulling 100% inside the canvasBox to remain visible.

* Use correct margins for YAxis.TextHorizontalAlign: chart.TextHorizontalAlignRight

* Fixed Show to Hidden due to 5f42a580a9
2019-12-06 11:22:51 -08:00
Will Charczuk
3a7bc55431 updates 2019-09-09 21:05:48 -07:00
Will Charczuk
60baf17927 updates 2019-09-09 21:04:05 -07:00
Will Charczuk
45fad0cfb8 switching to generators 2019-09-09 21:02:48 -07:00
Will Charczuk
2d5aeaf824 merging master 2019-09-09 20:24:15 -07:00
Will Charczuk
6d57cf4533 additions 2019-09-09 20:21:51 -07:00
Will Charczuk
602ff901f7 adds percent change series 2019-09-09 19:57:56 -07:00
Will Charczuk
fed210cc81 tweaks 2019-04-24 13:00:46 -07:00
Will Charczuk
762b314e86 removing go-sdk completely 2019-04-24 13:00:09 -07:00
Will Charczuk
07a9cdf513 removing go-sdk stuff 2019-04-24 12:58:05 -07:00
Alessandro
9852fce5a1 adding donut type chart, like a pie chart with a blank circle on the center and little trick for label position (#111)
(some way of improvement)
2019-02-19 10:52:03 -08:00
Justin Kromlinger
59451fbeb4 Add type classes on class output (#106)
* Add type classes on class output

Without this it is quite difficult to differentiate between fill and
stroke elements, f.e. with basic charts with fillings or legends
generally:
`svg path:nth-last-of-type(2).legend`

Text elements needed to be accessed with text.classname which
isn't really best practise.

This way they can be accessed easier:
`svg .legend.fill`

* Add type classes to examples

* Fix import in custom_stylesheets example
2019-02-19 10:51:41 -08:00
Will Charczuk
0576aba75e removing debugging file 2019-02-16 11:17:53 -08:00
Will Charczuk
781a45d770 tests pass 2019-02-16 11:17:39 -08:00
Will Charczuk
fa93bd8abb updates 2019-02-13 19:13:29 -08:00
Will Charczuk
5f42a580a9 mostly working 2019-02-13 18:55:13 -08:00
Will Charczuk
26eaa1d898 snapshot 2019-02-13 16:09:26 -08:00
Justin Kromlinger
3cb33d48d3 Add ability to set custom stylesheets for SVG renderer (#105)
* Add ability to set custom stylesheets for SVG renderer

This allow to set custom inline CSS and a optional CSP nonce. This
solves the problem mentioned in #103 and is best used with it, as seen
in the added examples. Without this one would have to write a custom
renderer.

* Add note with link to the custom_stylesheets example
2018-10-12 09:43:30 -07:00
Will Charczuk
96acfc6a9f switching the build badge 2018-10-12 09:35:03 -07:00
Will Charczuk
31d235310c removing travis references 2018-10-12 09:28:44 -07:00
Will Charczuk
70d5b73afd fixing build 2018-10-12 09:26:46 -07:00
Will Charczuk
f5889c93ae copy pasta 2018-10-12 09:24:20 -07:00
Will Charczuk
4e6c06ca87 removing coverage artifact 2018-10-12 09:23:56 -07:00
Will Charczuk
3e352f140b adds circle ci 2018-10-12 09:23:30 -07:00
Justin Kromlinger
f97f94425f Add ability to set CSS classes instead of inline styles (#103)
* Add ability to set CSS classes instead of inline styles

This allows to set a `ClassName` field in `Style` structs. Setting this
field to anything but "" will cause all other styles to be ignored. The
element will then have a `class=` tag instead with the corresponding name.

Possible reasons to use this:
* Including multiple SVGs on the same webside, using the same styles
* Desire to use strict CSP headers

* Add warning that setting `ClassName` will drop all other inline styles
2018-10-11 17:21:46 -07:00
Bernhard Reisenberger
6735e8990a draw circle if single value; do not position text on negative coordinates (#82) 2018-10-11 17:21:06 -07:00
nptrx
865ff54ab9 unification of sample and test coding styles will improve visibility (#67)
* "Style{Show: true}" to "StyleShow()"
* "Box{IsSet: true}" to "BoxZero"
2018-10-11 17:20:44 -07:00
Will Charczuk
0fb4aa53e9 adds a rerender example 2018-10-11 17:18:46 -07:00
MinJae Kwon
828d1952d8 Put a space between the badges (#101) 2018-10-04 23:57:51 -07:00
Will Charczuk
872b97b99f removing example 2018-09-10 13:14:46 -07:00
Will Charczuk
a7ff82d63f lots of changes 2018-09-10 13:11:25 -07:00
Will Charczuk
0e849b11bb sequence tweaks, removing market hours anything 2018-09-10 13:08:20 -07:00
Will Charczuk
1a09989055 fixing issues 2018-09-07 15:25:58 -07:00
Will Charczuk
1555902fc4 updates + tests 2018-09-07 12:52:30 -07:00
Will Charczuk
1144b80a46 latest go 2018-09-07 11:19:23 -07:00
Will Charczuk
1f159d195f adding IntValueFormatter 2018-09-07 11:17:11 -07:00
Yuji Yaginuma
4ed65028e4 Fix func name typo in examples (#91)
It seems that `RandomValuesWithMax` is correct.
9e3a080aa3/seq/random.go (L15)
2018-09-05 08:45:45 -07:00
David Mis
ac680bd82d Fixed order of arguments to assert.Len in test files. (#93)
* Fixed order of arguments to assert.Len in test files.

* Added BaseValue funtionality to bar chart
2018-09-05 08:45:19 -07:00
David Mis
3edccc4758 Added BaseValue funtionality to bar chart (#94) 2018-09-05 08:44:49 -07:00
Michael Bruce
d667b8c983 spelling correction (#98) 2018-09-05 08:43:52 -07:00
Andrew Poydence
0506f74600 Fix typo in example (#100) 2018-09-05 08:43:35 -07:00
Will Charczuk
9e3a080aa3 profanity tweaks 2018-04-15 16:53:01 -07:00
Will Charczuk
44990c63ed Merge branch 'master' of github.com:wcharczuk/go-chart 2018-04-15 12:43:40 -07:00
Will Charczuk
1ebbcf493d adding profanity checks 2018-04-15 12:43:32 -07:00
Nat Welch
62338336c3 Update .gitignore (#73)
* Delete .DS_Store

* Update .gitignore w/ common exclusions

From https://github.com/github/gitignore/blob/master/Go.gitignore
2018-04-15 12:36:03 -07:00
Edwin
2dc8482db3 allow 'zero y-range delta' (#72) 2018-04-15 12:35:39 -07:00
Will Charczuk
7c3982fe3d fixing tests 2018-04-05 00:47:39 -07:00
Will Charczuk
70e6cfddc5 fixing find and replace issue 2018-04-05 00:42:38 -07:00
Will Charczuk
df14434b6e changing assert 2018-04-05 00:36:12 -07:00
Will Charczuk
7d28470055 removing dep on go-util from blend 2018-04-04 22:06:34 -07:00
oneumyvakin
11e380634b Port changes 'fixing styling issues w/ the stack bar chart.' from commit a0ea012903 (#66) 2018-01-24 09:43:24 -08:00
Will Charczuk
f72f7fd57b fix for issues/56 2017-10-12 13:29:55 -07:00
221 changed files with 6263 additions and 4370 deletions

BIN
.DS_Store vendored

Binary file not shown.

33
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: "Continuous Integration"
on:
workflow_dispatch:
push:
branches: [ main ]
paths: [ "*.go" ]
pull_request:
branches: [ main ]
paths: [ "*.go" ]
jobs:
ci:
name: "Tests"
runs-on: ubuntu-latest
env:
GOOS: "linux"
GOARCH: "amd64"
GO111MODULE: "on"
CGO_ENABLED: "0"
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Check out go-incr
uses: actions/checkout@v3
- name: Run all tests
run: go test ./...

19
.gitignore vendored
View file

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

View file

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

1
COVERAGE Normal file
View file

@ -0,0 +1 @@
29.02

View file

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2016 William Charczuk. Copyright (c) 2016 William Charczuk.
Copyright (c) 2024 Zeni Kim.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,9 +1,10 @@
all: test all: new-install test
new-install:
@go get -v -u ./...
generate:
@go generate ./...
test: test:
@go test ./... @go test ./...
cover:
@go test -short -covermode=set -coverprofile=profile.cov
@go tool cover -html=profile.cov
@rm profile.cov

4
PROFANITY_RULES.yml Normal file
View file

@ -0,0 +1,4 @@
go-sdk:
excludeFiles: [ "*_test.go" ]
importsContain: [ github.com/blend/go-sdk/* ]
description: "please don't use go-sdk in this repo"

View file

@ -1,55 +1,55 @@
go-chart 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 This project starts from a full copy from [https://git.smarteching.com/zeni/go-chart](https://git.smarteching.com/zeni/go-chart). 28 Oct 2024.
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. Master should now be on the v3.x codebase, which overhauls the api significantly. Per usual, see `examples` for more information.
# Installation # Installation
To install `chart` run the following: To install `chart` run the following:
```bash ```bash
> go get -u github.com/wcharczuk/go-chart > go get git.smarteching.com/zeni/go-chart/v2@latest
``` ```
Most of the components are interchangeable so feel free to crib whatever you want. Most of the components are interchangeable so feel free to crib whatever you want.
# Output Examples # Output Examples
Spark Lines: Spark Lines:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/tvix_ltm.png) ![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/tvix_ltm.png)
Single axis: Single axis:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/goog_ltm.png) ![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/goog_ltm.png)
Two axis: Two axis:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/two_axis.png) ![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/two_axis.png)
# Other Chart Types # Other Chart Types
Pie Chart: Pie Chart:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/pie_chart.png) ![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/pie_chart.png)
The code for this chart can be found in `_examples/pie_chart/main.go`. The code for this chart can be found in `examples/pie_chart/main.go`.
Stacked Bar: Stacked Bar:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/stacked_bar.png) ![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/stacked_bar.png)
The code for this chart can be found in `_examples/stacked_bar/main.go`. The code for this chart can be found in `examples/stacked_bar/main.go`.
# Code Examples # Code Examples
Actual chart configurations and examples can be found in the `./_examples/` directory. They are web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output. Actual chart configurations and examples can be found in the `./examples/` directory. They are simple CLI programs that write to `output.png` (they are also updated with `go generate`.
If folder ends in "web", has web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output.
# Usage # Usage
@ -61,7 +61,7 @@ import (
... ...
"bytes" "bytes"
... ...
"github.com/wcharczuk/go-chart" //exposes "chart" "git.smarteching.com/zeni/go-chart/v2" //exposes "chart"
) )
graph := chart.Chart{ graph := chart.Chart{
@ -83,8 +83,7 @@ Here, we have a single series with x range values as float64s, rendered to a PNG
# API Overview # 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 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.
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. The best way to see the api in action is to look at the examples in the `./_examples/` directory.
@ -96,4 +95,4 @@ The goal with the API itself is to have the "zero value be useful", and to requi
# Contributions # Contributions
This library is super early but contributions are welcome. Contributions are welcome though this library is in a holding pattern for the forseable future.

147
_colors/colors_extended.txt Normal file
View file

@ -0,0 +1,147 @@
aliceblue #f0f8ff 240,248,255
antiquewhite #faebd7 250,235,215
aqua #00ffff 0,255,255
aquamarine #7fffd4 127,255,212
azure #f0ffff 240,255,255
beige #f5f5dc 245,245,220
bisque #ffe4c4 255,228,196
black #000000 0,0,0
blanchedalmond #ffebcd 255,235,205
blue #0000ff 0,0,255
blueviolet #8a2be2 138,43,226
brown #a52a2a 165,42,42
burlywood #deb887 222,184,135
cadetblue #5f9ea0 95,158,160
chartreuse #7fff00 127,255,0
chocolate #d2691e 210,105,30
coral #ff7f50 255,127,80
cornflowerblue #6495ed 100,149,237
cornsilk #fff8dc 255,248,220
crimson #dc143c 220,20,60
cyan #00ffff 0,255,255
darkblue #00008b 0,0,139
darkcyan #008b8b 0,139,139
darkgoldenrod #b8860b 184,134,11
darkgray #a9a9a9 169,169,169
darkgreen #006400 0,100,0
darkgrey #a9a9a9 169,169,169
darkkhaki #bdb76b 189,183,107
darkmagenta #8b008b 139,0,139
darkolivegreen #556b2f 85,107,47
darkorange #ff8c00 255,140,0
darkorchid #9932cc 153,50,204
darkred #8b0000 139,0,0
darksalmon #e9967a 233,150,122
darkseagreen #8fbc8f 143,188,143
darkslateblue #483d8b 72,61,139
darkslategray #2f4f4f 47,79,79
darkslategrey #2f4f4f 47,79,79
darkturquoise #00ced1 0,206,209
darkviolet #9400d3 148,0,211
deeppink #ff1493 255,20,147
deepskyblue #00bfff 0,191,255
dimgray #696969 105,105,105
dimgrey #696969 105,105,105
dodgerblue #1e90ff 30,144,255
firebrick #b22222 178,34,34
floralwhite #fffaf0 255,250,240
forestgreen #228b22 34,139,34
fuchsia #ff00ff 255,0,255
gainsboro #dcdcdc 220,220,220
ghostwhite #f8f8ff 248,248,255
gold #ffd700 255,215,0
goldenrod #daa520 218,165,32
gray #808080 128,128,128
green #008000 0,128,0
greenyellow #adff2f 173,255,47
grey #808080 128,128,128
honeydew #f0fff0 240,255,240
hotpink #ff69b4 255,105,180
indianred #cd5c5c 205,92,92
indigo #4b0082 75,0,130
ivory #fffff0 255,255,240
khaki #f0e68c 240,230,140
lavender #e6e6fa 230,230,250
lavenderblush #fff0f5 255,240,245
lawngreen #7cfc00 124,252,0
lemonchiffon #fffacd 255,250,205
lightblue #add8e6 173,216,230
lightcoral #f08080 240,128,128
lightcyan #e0ffff 224,255,255
lightgoldenrodyellow #fafad2 250,250,210
lightgray #d3d3d3 211,211,211
lightgreen #90ee90 144,238,144
lightgrey #d3d3d3 211,211,211
lightpink #ffb6c1 255,182,193
lightsalmon #ffa07a 255,160,122
lightseagreen #20b2aa 32,178,170
lightskyblue #87cefa 135,206,250
lightslategray #778899 119,136,153
lightslategrey #778899 119,136,153
lightsteelblue #b0c4de 176,196,222
lightyellow #ffffe0 255,255,224
lime #00ff00 0,255,0
limegreen #32cd32 50,205,50
linen #faf0e6 250,240,230
magenta #ff00ff 255,0,255
maroon #800000 128,0,0
mediumaquamarine #66cdaa 102,205,170
mediumblue #0000cd 0,0,205
mediumorchid #ba55d3 186,85,211
mediumpurple #9370db 147,112,219
mediumseagreen #3cb371 60,179,113
mediumslateblue #7b68ee 123,104,238
mediumspringgreen #00fa9a 0,250,154
mediumturquoise #48d1cc 72,209,204
mediumvioletred #c71585 199,21,133
midnightblue #191970 25,25,112
mintcream #f5fffa 245,255,250
mistyrose #ffe4e1 255,228,225
moccasin #ffe4b5 255,228,181
navajowhite #ffdead 255,222,173
navy #000080 0,0,128
oldlace #fdf5e6 253,245,230
olive #808000 128,128,0
olivedrab #6b8e23 107,142,35
orange #ffa500 255,165,0
orangered #ff4500 255,69,0
orchid #da70d6 218,112,214
palegoldenrod #eee8aa 238,232,170
palegreen #98fb98 152,251,152
paleturquoise #afeeee 175,238,238
palevioletred #db7093 219,112,147
papayawhip #ffefd5 255,239,213
peachpuff #ffdab9 255,218,185
peru #cd853f 205,133,63
pink #ffc0cb 255,192,203
plum #dda0dd 221,160,221
powderblue #b0e0e6 176,224,230
purple #800080 128,0,128
red #ff0000 255,0,0
rosybrown #bc8f8f 188,143,143
royalblue #4169e1 65,105,225
saddlebrown #8b4513 139,69,19
salmon #fa8072 250,128,114
sandybrown #f4a460 244,164,96
seagreen #2e8b57 46,139,87
seashell #fff5ee 255,245,238
sienna #a0522d 160,82,45
silver #c0c0c0 192,192,192
skyblue #87ceeb 135,206,235
slateblue #6a5acd 106,90,205
slategray #708090 112,128,144
slategrey #708090 112,128,144
snow #fffafa 255,250,250
springgreen #00ff7f 0,255,127
steelblue #4682b4 70,130,180
tan #d2b48c 210,180,140
teal #008080 0,128,128
thistle #d8bfd8 216,191,216
tomato #ff6347 255,99,71
turquoise #40e0d0 64,224,208
violet #ee82ee 238,130,238
wheat #f5deb3 245,222,179
white #ffffff 255,255,255
whitesmoke #f5f5f5 245,245,245
yellow #ffff00 255,255,0
yellowgreen #9acd32 154,205,50

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,43 +0,0 @@
package main
import (
"log"
"net/http"
"github.com/wcharczuk/go-chart"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func drawChartWide(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Width: 1920, //this overrides the default.
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0},
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/wide", drawChartWide)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,79 +0,0 @@
// Usage: http://localhost:8080?series=100&values=1000
package main
import (
"fmt"
"math/rand"
"net/http"
"strconv"
"time"
"github.com/wcharczuk/go-chart"
)
func random(min, max float64) float64 {
return rand.Float64()*(max-min) + min
}
func drawLargeChart(res http.ResponseWriter, r *http.Request) {
numSeriesInt64, err := strconv.ParseInt(r.FormValue("series"), 10, 64)
if err != nil {
numSeriesInt64 = int64(1)
}
if numSeriesInt64 == 0 {
numSeriesInt64 = 1
}
numSeries := int(numSeriesInt64)
numValuesInt64, err := strconv.ParseInt(r.FormValue("values"), 10, 64)
if err != nil {
numValuesInt64 = int64(100)
}
if numValuesInt64 == 0 {
numValuesInt64 = int64(100)
}
numValues := int(numValuesInt64)
series := make([]chart.Series, numSeries)
for i := 0; i < numSeries; i++ {
xValues := make([]time.Time, numValues)
yValues := make([]float64, numValues)
for j := 0; j < numValues; j++ {
xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1)
yValues[j] = random(float64(-500), float64(500))
}
series[i] = chart.TimeSeries{
Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i),
XValues: xValues,
YValues: yValues,
}
}
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "Time",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
YAxis: chart.YAxis{
Name: "Value",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
Series: series,
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawLargeChart)
http.HandleFunc("/favico.ico", func(res http.ResponseWriter, req *http.Request) {
res.Write([]byte{})
})
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,75 +0,0 @@
package main
import (
"net/http"
"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) {
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 50,
Left: 25,
Right: 25,
Bottom: 10,
},
FillColor: drawing.ColorFromHex("efefef"),
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: seq.Range(1.0, 100.0),
YValues: seq.RandomValuesWithMax(100, 512),
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func drawChartDefault(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{
Background: chart.Style{
FillColor: drawing.ColorFromHex("efefef"),
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: seq.Range(1.0, 100.0),
YValues: seq.RandomValuesWithMax(100, 512),
},
},
}
res.Header().Set("Content-Type", "image/png")
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.HandleFunc("/default", drawChartDefault)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View file

@ -1,46 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,137 +0,0 @@
package main
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/wcharczuk/go-chart"
util "github.com/wcharczuk/go-chart/util"
)
func parseInt(str string) int {
v, _ := strconv.Atoi(str)
return v
}
func parseFloat64(str string) float64 {
v, _ := strconv.ParseFloat(str, 64)
return v
}
func readData() ([]time.Time, []float64) {
var xvalues []time.Time
var yvalues []float64
err := util.File.ReadByLines("requests.csv", func(line string) error {
parts := strings.Split(line, ",")
year := parseInt(parts[0])
month := parseInt(parts[1])
day := parseInt(parts[2])
hour := parseInt(parts[3])
elapsedMillis := parseFloat64(parts[4])
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
yvalues = append(yvalues, elapsedMillis)
return nil
})
if err != nil {
fmt.Println(err.Error())
}
return xvalues, yvalues
}
func releases() []chart.GridLine {
return []chart.GridLine{
{Value: util.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
{Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
{Value: util.Time.ToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))},
{Value: util.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
{Value: util.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
{Value: util.Time.ToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
}
}
func drawChart(res http.ResponseWriter, req *http.Request) {
xvalues, yvalues := readData()
mainSeries := chart.TimeSeries{
Name: "Prod Request Timings",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorBlue,
FillColor: chart.ColorBlue.WithAlpha(100),
},
XValues: xvalues,
YValues: yvalues,
}
linreg := &chart.LinearRegressionSeries{
Name: "Linear Regression",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateBlue,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
sma := &chart.SMASeries{
Name: "SMA",
Style: chart.Style{
Show: true,
StrokeColor: chart.ColorRed,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
graph := chart.Chart{
Width: 1280,
Height: 720,
Background: chart.Style{
Padding: chart.Box{
Top: 50,
},
},
YAxis: chart.YAxis{
Name: "Elapsed Millis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
TickStyle: chart.Style{
TextRotationDegrees: 45.0,
},
ValueFormatter: func(v interface{}) string {
return fmt.Sprintf("%d ms", int(v.(float64)))
},
},
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
ValueFormatter: chart.TimeHourValueFormatter,
GridMajorStyle: chart.Style{
Show: true,
StrokeColor: chart.ColorAlternateGray,
StrokeWidth: 1.0,
},
GridLines: releases(),
},
Series: []chart.Series{
mainSeries,
linreg,
chart.LastValueAnnotation(linreg),
sma,
chart.LastValueAnnotation(sma),
},
}
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
res.Header().Set("Content-Type", chart.ContentTypePNG)
graph.Render(chart.PNG, res)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View file

@ -3,8 +3,11 @@ package chart
import ( import (
"fmt" "fmt"
"math" "math"
)
util "github.com/wcharczuk/go-chart/util" // Interface Assertions.
var (
_ Series = (*AnnotationSeries)(nil)
) )
// AnnotationSeries is a series of labels on the chart. // AnnotationSeries is a series of labels on the chart.
@ -50,17 +53,17 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
Right: 0, Right: 0,
Bottom: 0, Bottom: 0,
} }
if as.Style.IsZero() || as.Style.Show { if !as.Style.Hidden {
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
for _, a := range as.Annotations { for _, a := range as.Annotations {
style := a.Style.InheritFrom(seriesStyle) style := a.Style.InheritFrom(seriesStyle)
lx := canvasBox.Left + xrange.Translate(a.XValue) lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue) ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label) ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = util.Math.MinInt(box.Top, ab.Top) box.Top = MinInt(box.Top, ab.Top)
box.Left = util.Math.MinInt(box.Left, ab.Left) box.Left = MinInt(box.Left, ab.Left)
box.Right = util.Math.MaxInt(box.Right, ab.Right) box.Right = MaxInt(box.Right, ab.Right)
box.Bottom = util.Math.MaxInt(box.Bottom, ab.Bottom) box.Bottom = MaxInt(box.Bottom, ab.Bottom)
} }
} }
return box return box
@ -68,7 +71,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
// Render draws the series. // Render draws the series.
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
if as.Style.IsZero() || as.Style.Show { if !as.Style.Hidden {
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
for _, a := range as.Annotations { for _, a := range as.Annotations {
style := a.Style.InheritFrom(seriesStyle) style := a.Style.InheritFrom(seriesStyle)

View file

@ -4,17 +4,14 @@ import (
"image/color" "image/color"
"testing" "testing"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/drawing" "git.smarteching.com/zeni/go-chart/v2/testutil"
) )
func TestAnnotationSeriesMeasure(t *testing.T) { func TestAnnotationSeriesMeasure(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
as := AnnotationSeries{ as := AnnotationSeries{
Style: Style{
Show: true,
},
Annotations: []Value2{ Annotations: []Value2{
{XValue: 1.0, YValue: 1.0, Label: "1.0"}, {XValue: 1.0, YValue: 1.0, Label: "1.0"},
{XValue: 2.0, YValue: 2.0, Label: "2.0"}, {XValue: 2.0, YValue: 2.0, Label: "2.0"},
@ -24,10 +21,10 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
} }
r, err := PNG(110, 110) r, err := PNG(110, 110)
assert.Nil(err) testutil.AssertNil(t, err)
f, err := GetDefaultFont() f, err := GetDefaultFont()
assert.Nil(err) testutil.AssertNil(t, err)
xrange := &ContinuousRange{ xrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
@ -52,19 +49,18 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
} }
box := as.Measure(r, cb, xrange, yrange, sd) box := as.Measure(r, cb, xrange, yrange, sd)
assert.False(box.IsZero()) testutil.AssertFalse(t, box.IsZero())
assert.Equal(-5.0, box.Top) testutil.AssertEqual(t, -5.0, box.Top)
assert.Equal(5.0, box.Left) testutil.AssertEqual(t, 5.0, box.Left)
assert.Equal(146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px. testutil.AssertEqual(t, 146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px.
assert.Equal(115.0, box.Bottom) testutil.AssertEqual(t, 115.0, box.Bottom)
} }
func TestAnnotationSeriesRender(t *testing.T) { func TestAnnotationSeriesRender(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
as := AnnotationSeries{ as := AnnotationSeries{
Style: Style{ Style: Style{
Show: true,
FillColor: drawing.ColorWhite, FillColor: drawing.ColorWhite,
StrokeColor: drawing.ColorBlack, StrokeColor: drawing.ColorBlack,
}, },
@ -77,10 +73,10 @@ func TestAnnotationSeriesRender(t *testing.T) {
} }
r, err := PNG(110, 110) r, err := PNG(110, 110)
assert.Nil(err) testutil.AssertNil(t, err)
f, err := GetDefaultFont() f, err := GetDefaultFont()
assert.Nil(err) testutil.AssertNil(t, err)
xrange := &ContinuousRange{ xrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
@ -107,13 +103,13 @@ func TestAnnotationSeriesRender(t *testing.T) {
as.Render(r, cb, xrange, yrange, sd) as.Render(r, cb, xrange, yrange, sd)
rr, isRaster := r.(*rasterRenderer) rr, isRaster := r.(*rasterRenderer)
assert.True(isRaster) testutil.AssertTrue(t, isRaster)
assert.NotNil(rr) testutil.AssertNotNil(t, rr)
c := rr.i.At(38, 70) c := rr.i.At(38, 70)
converted, isRGBA := color.RGBAModel.Convert(c).(color.RGBA) converted, isRGBA := color.RGBAModel.Convert(c).(color.RGBA)
assert.True(isRGBA) testutil.AssertTrue(t, isRGBA)
assert.Equal(0, converted.R) testutil.AssertEqual(t, 0, converted.R)
assert.Equal(0, converted.G) testutil.AssertEqual(t, 0, converted.G)
assert.Equal(0, converted.B) testutil.AssertEqual(t, 0, converted.B)
} }

View file

@ -1,6 +1,11 @@
package seq package chart
// NewArray creates a new array. var (
_ Sequence = (*Array)(nil)
)
// NewArray returns a new array from a given set of values.
// Array implements Sequence, which allows it to be used with the sequence helpers.
func NewArray(values ...float64) Array { func NewArray(values ...float64) Array {
return Array(values) return Array(values)
} }

View file

@ -7,7 +7,6 @@ import (
"math" "math"
"github.com/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. // BarChart is a chart that draws bars on a range.
@ -31,6 +30,9 @@ type BarChart struct {
BarSpacing int BarSpacing int
UseBaseValue bool
BaseValue float64
Font *truetype.Font Font *truetype.Font
defaultFont *truetype.Font defaultFont *truetype.Font
@ -126,7 +128,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt) canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
yr = bc.setRangeDomains(canvasBox, yr) yr = bc.setRangeDomains(canvasBox, yr)
} }
bc.drawCanvas(r, canvasBox)
bc.drawBars(r, canvasBox, yr) bc.drawBars(r, canvasBox, yr)
bc.drawXAxis(r, canvasBox) bc.drawXAxis(r, canvasBox)
bc.drawYAxis(r, canvasBox, yr, yt) bc.drawYAxis(r, canvasBox, yr, yt)
@ -139,6 +141,10 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
return r.Save(w) return r.Save(w)
} }
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, bc.getCanvasStyle())
}
func (bc BarChart) getRanges() Range { func (bc BarChart) getRanges() Range {
var yrange Range var yrange Range
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() { if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
@ -195,11 +201,20 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
by = canvasBox.Bottom - yr.Translate(bar.Value) by = canvasBox.Bottom - yr.Translate(bar.Value)
barBox = Box{ if bc.UseBaseValue {
Top: by, barBox = Box{
Left: bxl, Top: by,
Right: bxr, Left: bxl,
Bottom: canvasBox.Bottom, Right: bxr,
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
}
} else {
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom,
}
} }
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index))) Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
@ -209,7 +224,7 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
} }
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) { func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
if bc.XAxis.Show { if !bc.XAxis.Hidden {
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes()) axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
axisStyle.WriteToRenderer(r) axisStyle.WriteToRenderer(r)
@ -248,44 +263,44 @@ func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
} }
func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) { func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
if bc.YAxis.Style.Show { if !bc.YAxis.Style.Hidden {
axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes()) bc.YAxis.Render(r, canvasBox, yr, bc.styleDefaultsAxes(), ticks)
axisStyle.WriteToRenderer(r)
r.MoveTo(canvasBox.Right, canvasBox.Top)
r.LineTo(canvasBox.Right, canvasBox.Bottom)
r.Stroke()
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
r.Stroke()
var ty int
var tb Box
for _, t := range ticks {
ty = canvasBox.Bottom - yr.Translate(t.Value)
axisStyle.GetStrokeOptions().WriteToRenderer(r)
r.MoveTo(canvasBox.Right, ty)
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
r.Stroke()
axisStyle.GetTextOptions().WriteToRenderer(r)
tb = r.MeasureText(t.Label)
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
}
} }
} }
func (bc BarChart) drawTitle(r Renderer) { func (bc BarChart) drawTitle(r Renderer) {
if len(bc.Title) > 0 && bc.TitleStyle.Show { if len(bc.Title) > 0 && !bc.TitleStyle.Hidden {
Draw.TextWithin(r, bc.Title, bc.box(), bc.styleDefaultsTitle()) r.SetFont(bc.TitleStyle.GetFont(bc.GetFont()))
r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor()))
titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize())
r.SetFontSize(titleFontSize)
textBox := r.MeasureText(bc.Title)
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (bc.GetWidth() >> 1) - (textWidth >> 1)
titleY := bc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
r.Text(bc.Title, titleX, titleY)
}
}
func (bc BarChart) getCanvasStyle() Style {
return bc.Canvas.InheritFrom(bc.styleDefaultsCanvas())
}
func (bc BarChart) styleDefaultsCanvas() Style {
return Style{
FillColor: bc.GetColorPalette().CanvasColor(),
StrokeColor: bc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultCanvasStrokeWidth,
} }
} }
func (bc BarChart) hasAxes() bool { func (bc BarChart) hasAxes() bool {
return bc.YAxis.Style.Show return !bc.YAxis.Style.Hidden
} }
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range { func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
@ -305,7 +320,7 @@ func (bc BarChart) getValueFormatters() ValueFormatter {
} }
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) { func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
if bc.YAxis.Style.Show { if !bc.YAxis.Style.Hidden {
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf) yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
} }
return return
@ -351,7 +366,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox) _, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
if bc.XAxis.Show { if !bc.XAxis.Hidden {
xaxisHeight := DefaultVerticalTickHeight xaxisHeight := DefaultVerticalTickHeight
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes()) axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
@ -369,7 +384,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle) lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
linesBox := Text.MeasureLines(r, lines, axisStyle) linesBox := Text.MeasureLines(r, lines, axisStyle)
xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight) xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
} }
} }
@ -383,7 +398,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
axesOuterBox = axesOuterBox.Grow(xbox) axesOuterBox = axesOuterBox.Grow(xbox)
} }
if bc.YAxis.Style.Show { if !bc.YAxis.Style.Hidden {
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks) axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
axesOuterBox = axesOuterBox.Grow(axesBounds) axesOuterBox = axesOuterBox.Grow(axesBounds)
} }
@ -397,8 +412,8 @@ func (bc BarChart) box() Box {
dpb := bc.Background.Padding.GetBottom(50) dpb := bc.Background.Padding.GetBottom(50)
return Box{ return Box{
Top: 20, Top: bc.Background.Padding.GetTop(20),
Left: 20, Left: bc.Background.Padding.GetLeft(20),
Right: bc.GetWidth() - dpr, Right: bc.GetWidth() - dpr,
Bottom: bc.GetHeight() - dpb, Bottom: bc.GetHeight() - dpb,
} }
@ -436,7 +451,7 @@ func (bc BarChart) styleDefaultsTitle() Style {
} }
func (bc BarChart) getTitleFontSize() float64 { func (bc BarChart) getTitleFontSize() float64 {
effectiveDimension := util.Math.MinInt(bc.GetWidth(), bc.GetHeight()) effectiveDimension := MinInt(bc.GetWidth(), bc.GetHeight())
if effectiveDimension >= 2048 { if effectiveDimension >= 2048 {
return 48 return 48
} else if effectiveDimension >= 1024 { } else if effectiveDimension >= 1024 {

View file

@ -5,20 +5,15 @@ import (
"math" "math"
"testing" "testing"
assert "github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
) )
func TestBarChartRender(t *testing.T) { func TestBarChartRender(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Width: 1024, Width: 1024,
Title: "Test Title", Title: "Test Title",
TitleStyle: StyleShow(),
XAxis: StyleShow(),
YAxis: YAxis{
Style: StyleShow(),
},
Bars: []Value{ Bars: []Value{
{Value: 1.0, Label: "One"}, {Value: 1.0, Label: "One"},
{Value: 2.0, Label: "Two"}, {Value: 2.0, Label: "Two"},
@ -30,21 +25,16 @@ func TestBarChartRender(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf) err := bc.Render(PNG, buf)
assert.Nil(err) testutil.AssertNil(t, err)
assert.NotZero(buf.Len()) testutil.AssertNotZero(t, buf.Len())
} }
func TestBarChartRenderZero(t *testing.T) { func TestBarChartRenderZero(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Width: 1024, Width: 1024,
Title: "Test Title", Title: "Test Title",
TitleStyle: StyleShow(),
XAxis: StyleShow(),
YAxis: YAxis{
Style: StyleShow(),
},
Bars: []Value{ Bars: []Value{
{Value: 0.0, Label: "One"}, {Value: 0.0, Label: "One"},
{Value: 0.0, Label: "Two"}, {Value: 0.0, Label: "Two"},
@ -53,64 +43,64 @@ func TestBarChartRenderZero(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf) err := bc.Render(PNG, buf)
assert.NotNil(err) testutil.AssertNotNil(t, err)
} }
func TestBarChartProps(t *testing.T) { func TestBarChartProps(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
assert.Equal(DefaultDPI, bc.GetDPI()) testutil.AssertEqual(t, DefaultDPI, bc.GetDPI())
bc.DPI = 100 bc.DPI = 100
assert.Equal(100, bc.GetDPI()) testutil.AssertEqual(t, 100, bc.GetDPI())
assert.Nil(bc.GetFont()) testutil.AssertNil(t, bc.GetFont())
f, err := GetDefaultFont() f, err := GetDefaultFont()
assert.Nil(err) testutil.AssertNil(t, err)
bc.Font = f bc.Font = f
assert.NotNil(bc.GetFont()) testutil.AssertNotNil(t, bc.GetFont())
assert.Equal(DefaultChartWidth, bc.GetWidth()) testutil.AssertEqual(t, DefaultChartWidth, bc.GetWidth())
bc.Width = DefaultChartWidth - 1 bc.Width = DefaultChartWidth - 1
assert.Equal(DefaultChartWidth-1, bc.GetWidth()) testutil.AssertEqual(t, DefaultChartWidth-1, bc.GetWidth())
assert.Equal(DefaultChartHeight, bc.GetHeight()) testutil.AssertEqual(t, DefaultChartHeight, bc.GetHeight())
bc.Height = DefaultChartHeight - 1 bc.Height = DefaultChartHeight - 1
assert.Equal(DefaultChartHeight-1, bc.GetHeight()) testutil.AssertEqual(t, DefaultChartHeight-1, bc.GetHeight())
assert.Equal(DefaultBarSpacing, bc.GetBarSpacing()) testutil.AssertEqual(t, DefaultBarSpacing, bc.GetBarSpacing())
bc.BarSpacing = 150 bc.BarSpacing = 150
assert.Equal(150, bc.GetBarSpacing()) testutil.AssertEqual(t, 150, bc.GetBarSpacing())
assert.Equal(DefaultBarWidth, bc.GetBarWidth()) testutil.AssertEqual(t, DefaultBarWidth, bc.GetBarWidth())
bc.BarWidth = 75 bc.BarWidth = 75
assert.Equal(75, bc.GetBarWidth()) testutil.AssertEqual(t, 75, bc.GetBarWidth())
} }
func TestBarChartRenderNoBars(t *testing.T) { func TestBarChartRenderNoBars(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
err := bc.Render(PNG, bytes.NewBuffer([]byte{})) err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
assert.NotNil(err) testutil.AssertNotNil(t, err)
} }
func TestBarChartGetRanges(t *testing.T) { func TestBarChartGetRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
yr := bc.getRanges() yr := bc.getRanges()
assert.NotNil(yr) testutil.AssertNotNil(t, yr)
assert.False(yr.IsZero()) testutil.AssertFalse(t, yr.IsZero())
assert.Equal(-math.MaxFloat64, yr.GetMax()) testutil.AssertEqual(t, -math.MaxFloat64, yr.GetMax())
assert.Equal(math.MaxFloat64, yr.GetMin()) testutil.AssertEqual(t, math.MaxFloat64, yr.GetMin())
} }
func TestBarChartGetRangesBarsMinMax(t *testing.T) { func TestBarChartGetRangesBarsMinMax(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Bars: []Value{ Bars: []Value{
@ -120,15 +110,15 @@ func TestBarChartGetRangesBarsMinMax(t *testing.T) {
} }
yr := bc.getRanges() yr := bc.getRanges()
assert.NotNil(yr) testutil.AssertNotNil(t, yr)
assert.False(yr.IsZero()) testutil.AssertFalse(t, yr.IsZero())
assert.Equal(10, yr.GetMax()) testutil.AssertEqual(t, 10, yr.GetMax())
assert.Equal(1, yr.GetMin()) testutil.AssertEqual(t, 1, yr.GetMin())
} }
func TestBarChartGetRangesMinMax(t *testing.T) { func TestBarChartGetRangesMinMax(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
YAxis: YAxis{ YAxis: YAxis{
@ -148,15 +138,15 @@ func TestBarChartGetRangesMinMax(t *testing.T) {
} }
yr := bc.getRanges() yr := bc.getRanges()
assert.NotNil(yr) testutil.AssertNotNil(t, yr)
assert.False(yr.IsZero()) testutil.AssertFalse(t, yr.IsZero())
assert.Equal(15, yr.GetMax()) testutil.AssertEqual(t, 15, yr.GetMax())
assert.Equal(5, yr.GetMin()) testutil.AssertEqual(t, 5, yr.GetMin())
} }
func TestBarChartGetRangesTicksMinMax(t *testing.T) { func TestBarChartGetRangesTicksMinMax(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
YAxis: YAxis{ YAxis: YAxis{
@ -172,57 +162,56 @@ func TestBarChartGetRangesTicksMinMax(t *testing.T) {
} }
yr := bc.getRanges() yr := bc.getRanges()
assert.NotNil(yr) testutil.AssertNotNil(t, yr)
assert.False(yr.IsZero()) testutil.AssertFalse(t, yr.IsZero())
assert.Equal(11, yr.GetMax()) testutil.AssertEqual(t, 11, yr.GetMax())
assert.Equal(7, yr.GetMin()) testutil.AssertEqual(t, 7, yr.GetMin())
} }
func TestBarChartHasAxes(t *testing.T) { func TestBarChartHasAxes(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
assert.False(bc.hasAxes()) testutil.AssertTrue(t, bc.hasAxes())
bc.YAxis = YAxis{ bc.YAxis = YAxis{
Style: StyleShow(), Style: Hidden(),
} }
testutil.AssertFalse(t, bc.hasAxes())
assert.True(bc.hasAxes())
} }
func TestBarChartGetDefaultCanvasBox(t *testing.T) { func TestBarChartGetDefaultCanvasBox(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
b := bc.getDefaultCanvasBox() b := bc.getDefaultCanvasBox()
assert.False(b.IsZero()) testutil.AssertFalse(t, b.IsZero())
} }
func TestBarChartSetRangeDomains(t *testing.T) { func TestBarChartSetRangeDomains(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
cb := bc.box() cb := bc.box()
yr := bc.getRanges() yr := bc.getRanges()
yr2 := bc.setRangeDomains(cb, yr) yr2 := bc.setRangeDomains(cb, yr)
assert.NotZero(yr2.GetDomain()) testutil.AssertNotZero(t, yr2.GetDomain())
} }
func TestBarChartGetValueFormatters(t *testing.T) { func TestBarChartGetValueFormatters(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{} bc := BarChart{}
vf := bc.getValueFormatters() vf := bc.getValueFormatters()
assert.NotNil(vf) testutil.AssertNotNil(t, vf)
assert.Equal("1234.00", vf(1234.0)) testutil.AssertEqual(t, "1234.00", vf(1234.0))
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" } bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
assert.Equal("test", bc.getValueFormatters()(1234)) testutil.AssertEqual(t, "test", bc.getValueFormatters()(1234))
} }
func TestBarChartGetAxesTicks(t *testing.T) { func TestBarChartGetAxesTicks(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Bars: []Value{ Bars: []Value{
@ -233,20 +222,21 @@ func TestBarChartGetAxesTicks(t *testing.T) {
} }
r, err := PNG(128, 128) r, err := PNG(128, 128)
assert.Nil(err) testutil.AssertNil(t, err)
yr := bc.getRanges() yr := bc.getRanges()
yf := bc.getValueFormatters() yf := bc.getValueFormatters()
bc.YAxis.Style.Hidden = true
ticks := bc.getAxesTicks(r, yr, yf) ticks := bc.getAxesTicks(r, yr, yf)
assert.Empty(ticks) testutil.AssertEmpty(t, ticks)
bc.YAxis.Style.Show = true bc.YAxis.Style.Hidden = false
ticks = bc.getAxesTicks(r, yr, yf) ticks = bc.getAxesTicks(r, yr, yf)
assert.Len(ticks, 2) testutil.AssertLen(t, ticks, 2)
} }
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) { func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Width: 1024, Width: 1024,
@ -261,15 +251,15 @@ func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
} }
spacing := bc.calculateEffectiveBarSpacing(bc.box()) spacing := bc.calculateEffectiveBarSpacing(bc.box())
assert.NotZero(spacing) testutil.AssertNotZero(t, spacing)
bc.BarWidth = 250 bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box()) spacing = bc.calculateEffectiveBarSpacing(bc.box())
assert.Zero(spacing) testutil.AssertZero(t, spacing)
} }
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) { func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
bc := BarChart{ bc := BarChart{
Width: 1024, Width: 1024,
@ -286,35 +276,35 @@ func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
cb := bc.box() cb := bc.box()
spacing := bc.calculateEffectiveBarSpacing(bc.box()) spacing := bc.calculateEffectiveBarSpacing(bc.box())
assert.NotZero(spacing) testutil.AssertNotZero(t, spacing)
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing) barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
assert.Equal(10, barWidth) testutil.AssertEqual(t, 10, barWidth)
bc.BarWidth = 250 bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box()) spacing = bc.calculateEffectiveBarSpacing(bc.box())
assert.Zero(spacing) testutil.AssertZero(t, spacing)
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing) barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
assert.Equal(199, barWidth) testutil.AssertEqual(t, 199, barWidth)
assert.Equal(cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing)) testutil.AssertEqual(t, cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
bw, bs, total := bc.calculateScaledTotalWidth(cb) bw, bs, total := bc.calculateScaledTotalWidth(cb)
assert.Equal(spacing, bs) testutil.AssertEqual(t, spacing, bs)
assert.Equal(barWidth, bw) testutil.AssertEqual(t, barWidth, bw)
assert.Equal(cb.Width()+1, total) testutil.AssertEqual(t, cb.Width()+1, total)
} }
func TestBarChatGetTitleFontSize(t *testing.T) { func TestBarChatGetTitleFontSize(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize() size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
assert.Equal(48, size) testutil.AssertEqual(t, 48, size)
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize() size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
assert.Equal(24, size) testutil.AssertEqual(t, 24, size)
size = BarChart{Width: 513, Height: 513}.getTitleFontSize() size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
assert.Equal(18, size) testutil.AssertEqual(t, 18, size)
size = BarChart{Width: 257, Height: 257}.getTitleFontSize() size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
assert.Equal(12, size) testutil.AssertEqual(t, 12, size)
size = BarChart{Width: 128, Height: 128}.getTitleFontSize() size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
assert.Equal(10, size) testutil.AssertEqual(t, 10, size)
} }

View file

@ -2,8 +2,11 @@ package chart
import ( import (
"fmt" "fmt"
)
"github.com/wcharczuk/go-chart/seq" // Interface Assertions.
var (
_ Series = (*BollingerBandsSeries)(nil)
) )
// BollingerBandsSeries draws bollinger bands for an inner series. // BollingerBandsSeries draws bollinger bands for an inner series.
@ -17,7 +20,7 @@ type BollingerBandsSeries struct {
K float64 K float64
InnerSeries ValuesProvider InnerSeries ValuesProvider
valueBuffer *seq.Buffer valueBuffer *ValueBuffer
} }
// GetName returns the name of the time series. // GetName returns the name of the time series.
@ -67,7 +70,7 @@ func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64)
return return
} }
if bbs.valueBuffer == nil || index == 0 { if bbs.valueBuffer == nil || index == 0 {
bbs.valueBuffer = seq.NewBufferWithCapacity(bbs.GetPeriod()) bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
} }
if bbs.valueBuffer.Len() >= bbs.GetPeriod() { if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
bbs.valueBuffer.Dequeue() bbs.valueBuffer.Dequeue()
@ -76,8 +79,8 @@ func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64)
bbs.valueBuffer.Enqueue(py) bbs.valueBuffer.Enqueue(py)
x = px x = px
ay := seq.New(bbs.valueBuffer).Average() ay := Seq{bbs.valueBuffer}.Average()
std := seq.New(bbs.valueBuffer).StdDev() std := Seq{bbs.valueBuffer}.StdDev()
y1 = ay + (bbs.GetK() * std) y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std)
@ -96,15 +99,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
startAt = 0 startAt = 0
} }
vb := seq.NewBufferWithCapacity(period) vb := NewValueBufferWithCapacity(period)
for index := startAt; index < seriesLength; index++ { for index := startAt; index < seriesLength; index++ {
xn, yn := bbs.InnerSeries.GetValues(index) xn, yn := bbs.InnerSeries.GetValues(index)
vb.Enqueue(yn) vb.Enqueue(yn)
x = xn x = xn
} }
ay := seq.Seq{Provider: vb}.Average() ay := Seq{vb}.Average()
std := seq.Seq{Provider: vb}.StdDev() std := Seq{vb}.StdDev()
y1 = ay + (bbs.GetK() * std) y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std)

View file

@ -5,16 +5,15 @@ import (
"math" "math"
"testing" "testing"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/seq"
) )
func TestBollingerBandSeries(t *testing.T) { func TestBollingerBandSeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
s1 := mockValuesProvider{ s1 := mockValuesProvider{
X: seq.Range(1.0, 100.0), X: LinearRange(1.0, 100.0),
Y: seq.RandomValuesWithMax(100, 1024), Y: RandomValuesWithMax(100, 1024),
} }
bbs := &BollingerBandsSeries{ bbs := &BollingerBandsSeries{
@ -30,16 +29,16 @@ func TestBollingerBandSeries(t *testing.T) {
} }
for x := bbs.GetPeriod(); x < 100; x++ { for x := bbs.GetPeriod(); x < 100; x++ {
assert.True(y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x])) testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
} }
} }
func TestBollingerBandLastValue(t *testing.T) { func TestBollingerBandLastValue(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
s1 := mockValuesProvider{ s1 := mockValuesProvider{
X: seq.Range(1.0, 100.0), X: LinearRange(1.0, 100.0),
Y: seq.Range(1.0, 100.0), Y: LinearRange(1.0, 100.0),
} }
bbs := &BollingerBandsSeries{ bbs := &BollingerBandsSeries{
@ -47,7 +46,7 @@ func TestBollingerBandLastValue(t *testing.T) {
} }
x, y1, y2 := bbs.GetBoundedLastValues() x, y1, y2 := bbs.GetBoundedLastValues()
assert.Equal(100.0, x) testutil.AssertEqual(t, 100.0, x)
assert.Equal(101, math.Floor(y1)) testutil.AssertEqual(t, 101, math.Floor(y1))
assert.Equal(83, math.Floor(y2)) testutil.AssertEqual(t, 83, math.Floor(y2))
} }

View file

@ -0,0 +1,36 @@
package chart
import "fmt"
// BoundedLastValuesAnnotationSeries returns a last value annotation series for a bounded values provider.
func BoundedLastValuesAnnotationSeries(innerSeries FullBoundedValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
lvx, lvy1, lvy2 := innerSeries.GetBoundedLastValues()
var vf ValueFormatter
if len(vfs) > 0 {
vf = vfs[0]
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
_, vf = typed.GetValueFormatters()
} else {
vf = FloatValueFormatter
}
label1 := vf(lvy1)
label2 := vf(lvy2)
var seriesName string
var seriesStyle Style
if typed, isTyped := innerSeries.(Series); isTyped {
seriesName = fmt.Sprintf("%s - Last Values", typed.GetName())
seriesStyle = typed.GetStyle()
}
return AnnotationSeries{
Name: seriesName,
Style: seriesStyle,
Annotations: []Value2{
{XValue: lvx, YValue: lvy1, Label: label1},
{XValue: lvx, YValue: lvy2, Label: label2},
},
}
}

72
box.go
View file

@ -3,8 +3,6 @@ package chart
import ( import (
"fmt" "fmt"
"math" "math"
util "github.com/wcharczuk/go-chart/util"
) )
var ( var (
@ -91,12 +89,12 @@ func (b Box) GetBottom(defaults ...int) int {
// Width returns the width // Width returns the width
func (b Box) Width() int { func (b Box) Width() int {
return util.Math.AbsInt(b.Right - b.Left) return AbsInt(b.Right - b.Left)
} }
// Height returns the height // Height returns the height
func (b Box) Height() int { func (b Box) Height() int {
return util.Math.AbsInt(b.Bottom - b.Top) return AbsInt(b.Bottom - b.Top)
} }
// Center returns the center of the box // Center returns the center of the box
@ -148,10 +146,10 @@ func (b Box) Equals(other Box) bool {
// Grow grows a box based on another box. // Grow grows a box based on another box.
func (b Box) Grow(other Box) Box { func (b Box) Grow(other Box) Box {
return Box{ return Box{
Top: util.Math.MinInt(b.Top, other.Top), Top: MinInt(b.Top, other.Top),
Left: util.Math.MinInt(b.Left, other.Left), Left: MinInt(b.Left, other.Left),
Right: util.Math.MaxInt(b.Right, other.Right), Right: MaxInt(b.Right, other.Right),
Bottom: util.Math.MaxInt(b.Bottom, other.Bottom), Bottom: MaxInt(b.Bottom, other.Bottom),
} }
} }
@ -222,10 +220,10 @@ func (b Box) Fit(other Box) Box {
func (b Box) Constrain(other Box) Box { func (b Box) Constrain(other Box) Box {
newBox := b.Clone() newBox := b.Clone()
newBox.Top = util.Math.MaxInt(newBox.Top, other.Top) newBox.Top = MaxInt(newBox.Top, other.Top)
newBox.Left = util.Math.MaxInt(newBox.Left, other.Left) newBox.Left = MaxInt(newBox.Left, other.Left)
newBox.Right = util.Math.MinInt(newBox.Right, other.Right) newBox.Right = MinInt(newBox.Right, other.Right)
newBox.Bottom = util.Math.MinInt(newBox.Bottom, other.Bottom) newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
return newBox return newBox
} }
@ -256,6 +254,22 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
return newBox return newBox
} }
func (b Box) Validate() error {
if b.Left < 0 {
return fmt.Errorf("invalid left; must be >= 0")
}
if b.Right < 0 {
return fmt.Errorf("invalid right; must be > 0")
}
if b.Top < 0 {
return fmt.Errorf("invalid top; must be > 0")
}
if b.Bottom < 0 {
return fmt.Errorf("invalid bottom; must be > 0")
}
return nil
}
// BoxCorners is a box with independent corners. // BoxCorners is a box with independent corners.
type BoxCorners struct { type BoxCorners struct {
TopLeft, TopRight, BottomRight, BottomLeft Point TopLeft, TopRight, BottomRight, BottomLeft Point
@ -264,36 +278,36 @@ type BoxCorners struct {
// Box return the BoxCorners as a regular box. // Box return the BoxCorners as a regular box.
func (bc BoxCorners) Box() Box { func (bc BoxCorners) Box() Box {
return Box{ return Box{
Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y), Top: MinInt(bc.TopLeft.Y, bc.TopRight.Y),
Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X), Left: MinInt(bc.TopLeft.X, bc.BottomLeft.X),
Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X), Right: MaxInt(bc.TopRight.X, bc.BottomRight.X),
Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y), Bottom: MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
} }
} }
// Width returns the width // Width returns the width
func (bc BoxCorners) Width() int { func (bc BoxCorners) Width() int {
minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X) minLeft := MinInt(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X) maxRight := MaxInt(bc.TopRight.X, bc.BottomRight.X)
return maxRight - minLeft return maxRight - minLeft
} }
// Height returns the height // Height returns the height
func (bc BoxCorners) Height() int { func (bc BoxCorners) Height() int {
minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y) minTop := MinInt(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y) maxBottom := MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
return maxBottom - minTop return maxBottom - minTop
} }
// Center returns the center of the box // Center returns the center of the box
func (bc BoxCorners) Center() (x, y int) { func (bc BoxCorners) Center() (x, y int) {
left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X) left := MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X) right := MeanInt(bc.TopRight.X, bc.BottomRight.X)
x = ((right - left) >> 1) + left x = ((right - left) >> 1) + left
top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y) top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y) bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
y = ((bottom - top) >> 1) + top y = ((bottom - top) >> 1) + top
return return
@ -303,12 +317,12 @@ func (bc BoxCorners) Center() (x, y int) {
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners { func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
cx, cy := bc.Center() cx, cy := bc.Center()
thetaRadians := util.Math.DegreesToRadians(thetaDegrees) thetaRadians := DegreesToRadians(thetaDegrees)
tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians) tlx, tly := RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians) trx, try := RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians) brx, bry := RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians) blx, bly := RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
return BoxCorners{ return BoxCorners{
TopLeft: Point{tlx, tly}, TopLeft: Point{tlx, tly},

View file

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

View file

@ -7,7 +7,6 @@ import (
"math" "math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
util "github.com/wcharczuk/go-chart/util"
) )
// Chart is what we're drawing. // Chart is what we're drawing.
@ -33,6 +32,8 @@ type Chart struct {
Series []Series Series []Series
Elements []Renderable Elements []Renderable
Log Logger
} }
// GetDPI returns the dpi for the chart. // GetDPI returns the dpi for the chart.
@ -75,8 +76,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
if len(c.Series) == 0 { if len(c.Series) == 0 {
return errors.New("please provide at least one series") return errors.New("please provide at least one series")
} }
if visibleSeriesErr := c.checkHasVisibleSeries(); visibleSeriesErr != nil { if err := c.checkHasVisibleSeries(); err != nil {
return visibleSeriesErr return err
} }
c.YAxisSecondary.AxisType = YAxisSecondary c.YAxisSecondary.AxisType = YAxisSecondary
@ -102,6 +103,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
canvasBox := c.getDefaultCanvasBox() canvasBox := c.getDefaultCanvasBox()
xf, yf, yfa := c.getValueFormatters() xf, yf, yfa := c.getValueFormatters()
Debugf(c.Log, "chart; canvas box: %v", canvasBox)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra) xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
err = c.checkRanges(xr, yr, yra) err = c.checkRanges(xr, yr, yra)
@ -115,6 +118,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta) canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra) xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
Debugf(c.Log, "chart; axes adjusted canvas box: %v", canvasBox)
// do a second pass in case things haven't settled yet. // do a second pass in case things haven't settled yet.
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa) xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta) canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
@ -125,6 +130,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa) canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra) xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa) xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
Debugf(c.Log, "chart; annotation adjusted canvas box: %v", canvasBox)
} }
c.drawCanvas(r, canvasBox) c.drawCanvas(r, canvasBox)
@ -143,16 +150,14 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
} }
func (c Chart) checkHasVisibleSeries() error { func (c Chart) checkHasVisibleSeries() error {
hasVisibleSeries := false
var style Style var style Style
for _, s := range c.Series { for _, s := range c.Series {
style = s.GetStyle() style = s.GetStyle()
hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show) if !style.Hidden {
return nil
}
} }
if !hasVisibleSeries { return fmt.Errorf("chart render; must have (1) visible series")
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
}
return nil
} }
func (c Chart) validateSeries() error { func (c Chart) validateSeries() error {
@ -176,7 +181,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
// note: a possible future optimization is to not scan the series values if // note: a possible future optimization is to not scan the series values if
// all axis are represented by either custom ticks or custom ranges. // all axis are represented by either custom ticks or custom ranges.
for _, s := range c.Series { for _, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show { if !s.GetStyle().Hidden {
seriesAxis := s.GetYAxis() seriesAxis := s.GetYAxis()
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider { if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
seriesLength := bvp.Len() seriesLength := bvp.Len()
@ -263,11 +268,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrange.SetMin(miny) yrange.SetMin(miny)
yrange.SetMax(maxy) yrange.SetMax(maxy)
// only round if we're showing the axis if !c.YAxis.Style.Hidden {
if c.YAxis.Style.Show {
delta := yrange.GetDelta() delta := yrange.GetDelta()
roundTo := util.Math.GetRoundToForDelta(delta) roundTo := GetRoundToForDelta(delta)
rmin, rmax := util.Math.RoundDown(yrange.GetMin(), roundTo), util.Math.RoundUp(yrange.GetMax(), roundTo) rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
yrange.SetMin(rmin) yrange.SetMin(rmin)
yrange.SetMax(rmax) yrange.SetMax(rmax)
@ -286,10 +290,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrangeAlt.SetMin(minya) yrangeAlt.SetMin(minya)
yrangeAlt.SetMax(maxya) yrangeAlt.SetMax(maxya)
if c.YAxisSecondary.Style.Show { if !c.YAxisSecondary.Style.Hidden {
delta := yrangeAlt.GetDelta() delta := yrangeAlt.GetDelta()
roundTo := util.Math.GetRoundToForDelta(delta) roundTo := GetRoundToForDelta(delta)
rmin, rmax := util.Math.RoundDown(yrangeAlt.GetMin(), roundTo), util.Math.RoundUp(yrangeAlt.GetMax(), roundTo) rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
yrangeAlt.SetMin(rmin) yrangeAlt.SetMin(rmin)
yrangeAlt.SetMax(rmax) yrangeAlt.SetMax(rmax)
} }
@ -299,6 +303,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
} }
func (c Chart) checkRanges(xr, yr, yra Range) error { func (c Chart) checkRanges(xr, yr, yra Range) error {
Debugf(c.Log, "checking xrange: %v", xr)
xDelta := xr.GetDelta() xDelta := xr.GetDelta()
if math.IsInf(xDelta, 0) { if math.IsInf(xDelta, 0) {
return errors.New("infinite x-range delta") return errors.New("infinite x-range delta")
@ -310,6 +315,7 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
return errors.New("zero x-range delta; there needs to be at least (2) values") return errors.New("zero x-range delta; there needs to be at least (2) values")
} }
Debugf(c.Log, "checking yrange: %v", yr)
yDelta := yr.GetDelta() yDelta := yr.GetDelta()
if math.IsInf(yDelta, 0) { if math.IsInf(yDelta, 0) {
return errors.New("infinite y-range delta") return errors.New("infinite y-range delta")
@ -317,11 +323,9 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
if math.IsNaN(yDelta) { if math.IsNaN(yDelta) {
return errors.New("nan y-range delta") return errors.New("nan y-range delta")
} }
if yDelta == 0 {
return errors.New("zero y-range delta")
}
if c.hasSecondarySeries() { if c.hasSecondarySeries() {
Debugf(c.Log, "checking secondary yrange: %v", yra)
yraDelta := yra.GetDelta() yraDelta := yra.GetDelta()
if math.IsInf(yraDelta, 0) { if math.IsInf(yraDelta, 0) {
return errors.New("infinite secondary y-range delta") return errors.New("infinite secondary y-range delta")
@ -329,9 +333,6 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
if math.IsNaN(yraDelta) { if math.IsNaN(yraDelta) {
return errors.New("nan secondary y-range delta") return errors.New("nan secondary y-range delta")
} }
if yraDelta == 0 {
return errors.New("zero secondary y-range delta")
}
} }
return nil return nil
@ -367,17 +368,17 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
} }
func (c Chart) hasAxes() bool { func (c Chart) hasAxes() bool {
return c.XAxis.Style.Show || c.YAxis.Style.Show || c.YAxisSecondary.Style.Show return !c.XAxis.Style.Hidden || !c.YAxis.Style.Hidden || !c.YAxisSecondary.Style.Hidden
} }
func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) { func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) {
if c.XAxis.Style.Show { if !c.XAxis.Style.Hidden {
xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf) xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf)
} }
if c.YAxis.Style.Show { if !c.YAxis.Style.Hidden {
yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf) yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
} }
if c.YAxisSecondary.Style.Show { if !c.YAxisSecondary.Style.Hidden {
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa) yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
} }
return return
@ -385,16 +386,19 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box { func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
axesOuterBox := canvasBox.Clone() axesOuterBox := canvasBox.Clone()
if c.XAxis.Style.Show { if !c.XAxis.Style.Hidden {
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks) axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
Debugf(c.Log, "chart; x-axis measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds) axesOuterBox = axesOuterBox.Grow(axesBounds)
} }
if c.YAxis.Style.Show { if !c.YAxis.Style.Hidden {
axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks) axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks)
Debugf(c.Log, "chart; y-axis measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds) axesOuterBox = axesOuterBox.Grow(axesBounds)
} }
if c.YAxisSecondary.Style.Show { if !c.YAxisSecondary.Style.Hidden && c.hasSecondarySeries() {
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt) axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt)
Debugf(c.Log, "chart; y-axis secondary measured %v", axesBounds)
axesOuterBox = axesOuterBox.Grow(axesBounds) axesOuterBox = axesOuterBox.Grow(axesBounds)
} }
@ -411,7 +415,7 @@ func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range,
func (c Chart) hasAnnotationSeries() bool { func (c Chart) hasAnnotationSeries() bool {
for _, s := range c.Series { for _, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries { if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if as.Style.IsZero() || as.Style.Show { if !as.GetStyle().Hidden {
return true return true
} }
} }
@ -432,7 +436,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
annotationSeriesBox := canvasBox.Clone() annotationSeriesBox := canvasBox.Clone()
for seriesIndex, s := range c.Series { for seriesIndex, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries { if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if as.Style.IsZero() || as.Style.Show { if !as.GetStyle().Hidden {
style := c.styleDefaultsSeries(seriesIndex) style := c.styleDefaultsSeries(seriesIndex)
var annotationBounds Box var annotationBounds Box
if as.YAxis == YAxisPrimary { if as.YAxis == YAxisPrimary {
@ -469,19 +473,19 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
} }
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) { func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
if c.XAxis.Style.Show { if !c.XAxis.Style.Hidden {
c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks) c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
} }
if c.YAxis.Style.Show { if !c.YAxis.Style.Hidden {
c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks) c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
} }
if c.YAxisSecondary.Style.Show { if !c.YAxisSecondary.Style.Hidden {
c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt) c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt)
} }
} }
func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) { func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
if s.GetStyle().IsZero() || s.GetStyle().Show { if !s.GetStyle().Hidden {
if s.GetYAxis() == YAxisPrimary { if s.GetYAxis() == YAxisPrimary {
s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex)) s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
} else if s.GetYAxis() == YAxisSecondary { } else if s.GetYAxis() == YAxisSecondary {
@ -491,7 +495,7 @@ func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt R
} }
func (c Chart) drawTitle(r Renderer) { func (c Chart) drawTitle(r Renderer) {
if len(c.Title) > 0 && c.TitleStyle.Show { if len(c.Title) > 0 && !c.TitleStyle.Hidden {
r.SetFont(c.TitleStyle.GetFont(c.GetFont())) r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor())) r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize) titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)

View file

@ -8,58 +8,57 @@ import (
"testing" "testing"
"time" "time"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/drawing" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/seq"
) )
func TestChartGetDPI(t *testing.T) { func TestChartGetDPI(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
unset := Chart{} unset := Chart{}
assert.Equal(DefaultDPI, unset.GetDPI()) testutil.AssertEqual(t, DefaultDPI, unset.GetDPI())
assert.Equal(192, unset.GetDPI(192)) testutil.AssertEqual(t, 192, unset.GetDPI(192))
set := Chart{DPI: 128} set := Chart{DPI: 128}
assert.Equal(128, set.GetDPI()) testutil.AssertEqual(t, 128, set.GetDPI())
assert.Equal(128, set.GetDPI(192)) testutil.AssertEqual(t, 128, set.GetDPI(192))
} }
func TestChartGetFont(t *testing.T) { func TestChartGetFont(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
f, err := GetDefaultFont() f, err := GetDefaultFont()
assert.Nil(err) testutil.AssertNil(t, err)
unset := Chart{} unset := Chart{}
assert.Nil(unset.GetFont()) testutil.AssertNil(t, unset.GetFont())
set := Chart{Font: f} set := Chart{Font: f}
assert.NotNil(set.GetFont()) testutil.AssertNotNil(t, set.GetFont())
} }
func TestChartGetWidth(t *testing.T) { func TestChartGetWidth(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
unset := Chart{} unset := Chart{}
assert.Equal(DefaultChartWidth, unset.GetWidth()) testutil.AssertEqual(t, DefaultChartWidth, unset.GetWidth())
set := Chart{Width: DefaultChartWidth + 10} set := Chart{Width: DefaultChartWidth + 10}
assert.Equal(DefaultChartWidth+10, set.GetWidth()) testutil.AssertEqual(t, DefaultChartWidth+10, set.GetWidth())
} }
func TestChartGetHeight(t *testing.T) { func TestChartGetHeight(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
unset := Chart{} unset := Chart{}
assert.Equal(DefaultChartHeight, unset.GetHeight()) testutil.AssertEqual(t, DefaultChartHeight, unset.GetHeight())
set := Chart{Height: DefaultChartHeight + 10} set := Chart{Height: DefaultChartHeight + 10}
assert.Equal(DefaultChartHeight+10, set.GetHeight()) testutil.AssertEqual(t, DefaultChartHeight+10, set.GetHeight())
} }
func TestChartGetRanges(t *testing.T) { func TestChartGetRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Series: []Series{ Series: []Series{
@ -80,14 +79,14 @@ func TestChartGetRanges(t *testing.T) {
} }
xrange, yrange, yrangeAlt := c.getRanges() xrange, yrange, yrangeAlt := c.getRanges()
assert.Equal(-2.0, xrange.GetMin()) testutil.AssertEqual(t, -2.0, xrange.GetMin())
assert.Equal(5.0, xrange.GetMax()) testutil.AssertEqual(t, 5.0, xrange.GetMax())
assert.Equal(-2.1, yrange.GetMin()) testutil.AssertEqual(t, -2.1, yrange.GetMin())
assert.Equal(4.5, yrange.GetMax()) testutil.AssertEqual(t, 4.5, yrange.GetMax())
assert.Equal(10.0, yrangeAlt.GetMin()) testutil.AssertEqual(t, 10.0, yrangeAlt.GetMin())
assert.Equal(14.0, yrangeAlt.GetMax()) testutil.AssertEqual(t, 14.0, yrangeAlt.GetMax())
cSet := Chart{ cSet := Chart{
XAxis: XAxis{ XAxis: XAxis{
@ -117,18 +116,18 @@ func TestChartGetRanges(t *testing.T) {
} }
xr2, yr2, yra2 := cSet.getRanges() xr2, yr2, yra2 := cSet.getRanges()
assert.Equal(9.8, xr2.GetMin()) testutil.AssertEqual(t, 9.8, xr2.GetMin())
assert.Equal(19.8, xr2.GetMax()) testutil.AssertEqual(t, 19.8, xr2.GetMax())
assert.Equal(9.9, yr2.GetMin()) testutil.AssertEqual(t, 9.9, yr2.GetMin())
assert.Equal(19.9, yr2.GetMax()) testutil.AssertEqual(t, 19.9, yr2.GetMax())
assert.Equal(9.7, yra2.GetMin()) testutil.AssertEqual(t, 9.7, yra2.GetMin())
assert.Equal(19.7, yra2.GetMax()) testutil.AssertEqual(t, 19.7, yra2.GetMax())
} }
func TestChartGetRangesUseTicks(t *testing.T) { func TestChartGetRangesUseTicks(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
// this test asserts that ticks should supercede manual ranges when generating the overall ranges. // this test asserts that ticks should supercede manual ranges when generating the overall ranges.
@ -156,15 +155,15 @@ func TestChartGetRangesUseTicks(t *testing.T) {
} }
xr, yr, yar := c.getRanges() xr, yr, yar := c.getRanges()
assert.Equal(-2.0, xr.GetMin()) testutil.AssertEqual(t, -2.0, xr.GetMin())
assert.Equal(2.0, xr.GetMax()) testutil.AssertEqual(t, 2.0, xr.GetMax())
assert.Equal(0.0, yr.GetMin()) testutil.AssertEqual(t, 0.0, yr.GetMin())
assert.Equal(5.0, yr.GetMax()) testutil.AssertEqual(t, 5.0, yr.GetMax())
assert.True(yar.IsZero(), yar.String()) testutil.AssertTrue(t, yar.IsZero(), yar.String())
} }
func TestChartGetRangesUseUserRanges(t *testing.T) { func TestChartGetRangesUseUserRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
YAxis: YAxis{ YAxis: YAxis{
@ -182,15 +181,15 @@ func TestChartGetRangesUseUserRanges(t *testing.T) {
} }
xr, yr, yar := c.getRanges() xr, yr, yar := c.getRanges()
assert.Equal(-2.0, xr.GetMin()) testutil.AssertEqual(t, -2.0, xr.GetMin())
assert.Equal(2.0, xr.GetMax()) testutil.AssertEqual(t, 2.0, xr.GetMax())
assert.Equal(-5.0, yr.GetMin()) testutil.AssertEqual(t, -5.0, yr.GetMin())
assert.Equal(5.0, yr.GetMax()) testutil.AssertEqual(t, 5.0, yr.GetMax())
assert.True(yar.IsZero(), yar.String()) testutil.AssertTrue(t, yar.IsZero(), yar.String())
} }
func TestChartGetBackgroundStyle(t *testing.T) { func TestChartGetBackgroundStyle(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Background: Style{ Background: Style{
@ -199,11 +198,11 @@ func TestChartGetBackgroundStyle(t *testing.T) {
} }
bs := c.getBackgroundStyle() bs := c.getBackgroundStyle()
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String()) testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
} }
func TestChartGetCanvasStyle(t *testing.T) { func TestChartGetCanvasStyle(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Canvas: Style{ Canvas: Style{
@ -212,19 +211,19 @@ func TestChartGetCanvasStyle(t *testing.T) {
} }
bs := c.getCanvasStyle() bs := c.getCanvasStyle()
assert.Equal(bs.FillColor.String(), drawing.ColorBlack.String()) testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
} }
func TestChartGetDefaultCanvasBox(t *testing.T) { func TestChartGetDefaultCanvasBox(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{} c := Chart{}
canvasBoxDefault := c.getDefaultCanvasBox() canvasBoxDefault := c.getDefaultCanvasBox()
assert.False(canvasBoxDefault.IsZero()) testutil.AssertFalse(t, canvasBoxDefault.IsZero())
assert.Equal(DefaultBackgroundPadding.Top, canvasBoxDefault.Top) testutil.AssertEqual(t, DefaultBackgroundPadding.Top, canvasBoxDefault.Top)
assert.Equal(DefaultBackgroundPadding.Left, canvasBoxDefault.Left) testutil.AssertEqual(t, DefaultBackgroundPadding.Left, canvasBoxDefault.Left)
assert.Equal(c.GetWidth()-DefaultBackgroundPadding.Right, canvasBoxDefault.Right) testutil.AssertEqual(t, c.GetWidth()-DefaultBackgroundPadding.Right, canvasBoxDefault.Right)
assert.Equal(c.GetHeight()-DefaultBackgroundPadding.Bottom, canvasBoxDefault.Bottom) testutil.AssertEqual(t, c.GetHeight()-DefaultBackgroundPadding.Bottom, canvasBoxDefault.Bottom)
custom := Chart{ custom := Chart{
Background: Style{ Background: Style{
@ -237,15 +236,15 @@ func TestChartGetDefaultCanvasBox(t *testing.T) {
}, },
} }
canvasBoxCustom := custom.getDefaultCanvasBox() canvasBoxCustom := custom.getDefaultCanvasBox()
assert.False(canvasBoxCustom.IsZero()) testutil.AssertFalse(t, canvasBoxCustom.IsZero())
assert.Equal(DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top) testutil.AssertEqual(t, DefaultBackgroundPadding.Top+1, canvasBoxCustom.Top)
assert.Equal(DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left) testutil.AssertEqual(t, DefaultBackgroundPadding.Left+1, canvasBoxCustom.Left)
assert.Equal(c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right) testutil.AssertEqual(t, c.GetWidth()-(DefaultBackgroundPadding.Right+1), canvasBoxCustom.Right)
assert.Equal(c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom) testutil.AssertEqual(t, c.GetHeight()-(DefaultBackgroundPadding.Bottom+1), canvasBoxCustom.Bottom)
} }
func TestChartGetValueFormatters(t *testing.T) { func TestChartGetValueFormatters(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Series: []Series{ Series: []Series{
@ -266,90 +265,95 @@ func TestChartGetValueFormatters(t *testing.T) {
} }
dxf, dyf, dyaf := c.getValueFormatters() dxf, dyf, dyaf := c.getValueFormatters()
assert.NotNil(dxf) testutil.AssertNotNil(t, dxf)
assert.NotNil(dyf) testutil.AssertNotNil(t, dyf)
assert.NotNil(dyaf) testutil.AssertNotNil(t, dyaf)
} }
func TestChartHasAxes(t *testing.T) { func TestChartHasAxes(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
assert.False(Chart{}.hasAxes()) testutil.AssertTrue(t, Chart{}.hasAxes())
testutil.AssertFalse(t, Chart{XAxis: XAxis{Style: Hidden()}, YAxis: YAxis{Style: Hidden()}, YAxisSecondary: YAxis{Style: Hidden()}}.hasAxes())
x := Chart{ x := Chart{
XAxis: XAxis{ XAxis: XAxis{
Style: Style{ Style: Hidden(),
Show: true, },
}, YAxis: YAxis{
Style: Shown(),
},
YAxisSecondary: YAxis{
Style: Hidden(),
}, },
} }
assert.True(x.hasAxes()) testutil.AssertTrue(t, x.hasAxes())
y := Chart{ y := Chart{
XAxis: XAxis{
Style: Shown(),
},
YAxis: YAxis{ YAxis: YAxis{
Style: Style{ Style: Hidden(),
Show: true, },
}, YAxisSecondary: YAxis{
Style: Hidden(),
}, },
} }
assert.True(y.hasAxes()) testutil.AssertTrue(t, y.hasAxes())
ya := Chart{ ya := Chart{
XAxis: XAxis{
Style: Hidden(),
},
YAxis: YAxis{
Style: Hidden(),
},
YAxisSecondary: YAxis{ YAxisSecondary: YAxis{
Style: Style{ Style: Shown(),
Show: true,
},
}, },
} }
assert.True(ya.hasAxes()) testutil.AssertTrue(t, ya.hasAxes())
} }
func TestChartGetAxesTicks(t *testing.T) { func TestChartGetAxesTicks(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
r, err := PNG(1024, 1024) r, err := PNG(1024, 1024)
assert.Nil(err) testutil.AssertNil(t, err)
c := Chart{ c := Chart{
XAxis: XAxis{ XAxis: XAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.8, Max: 19.8}, Range: &ContinuousRange{Min: 9.8, Max: 19.8},
}, },
YAxis: YAxis{ YAxis: YAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.9, Max: 19.9}, Range: &ContinuousRange{Min: 9.9, Max: 19.9},
}, },
YAxisSecondary: YAxis{ YAxisSecondary: YAxis{
Style: Style{Show: true},
Range: &ContinuousRange{Min: 9.7, Max: 19.7}, Range: &ContinuousRange{Min: 9.7, Max: 19.7},
}, },
} }
xr, yr, yar := c.getRanges() xr, yr, yar := c.getRanges()
xt, yt, yat := c.getAxesTicks(r, xr, yr, yar, FloatValueFormatter, FloatValueFormatter, FloatValueFormatter) xt, yt, yat := c.getAxesTicks(r, xr, yr, yar, FloatValueFormatter, FloatValueFormatter, FloatValueFormatter)
assert.NotEmpty(xt) testutil.AssertNotEmpty(t, xt)
assert.NotEmpty(yt) testutil.AssertNotEmpty(t, yt)
assert.NotEmpty(yat) testutil.AssertNotEmpty(t, yat)
} }
func TestChartSingleSeries(t *testing.T) { func TestChartSingleSeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
now := time.Now() now := time.Now()
c := Chart{ c := Chart{
Title: "Hello!", Title: "Hello!",
TitleStyle: StyleShow(), Width: 1024,
Width: 1024, Height: 400,
Height: 400,
YAxis: YAxis{ YAxis: YAxis{
Style: StyleShow(),
Range: &ContinuousRange{ Range: &ContinuousRange{
Min: 0.0, Min: 0.0,
Max: 4.0, Max: 4.0,
}, },
}, },
XAxis: XAxis{
Style: StyleShow(),
},
Series: []Series{ Series: []Series{
TimeSeries{ TimeSeries{
Name: "goog", Name: "goog",
@ -361,11 +365,11 @@ func TestChartSingleSeries(t *testing.T) {
buffer := bytes.NewBuffer([]byte{}) buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer) c.Render(PNG, buffer)
assert.NotEmpty(buffer.Bytes()) testutil.AssertNotEmpty(t, buffer.Bytes())
} }
func TestChartRegressionBadRanges(t *testing.T) { func TestChartRegressionBadRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Series: []Series{ Series: []Series{
@ -377,11 +381,11 @@ func TestChartRegressionBadRanges(t *testing.T) {
} }
buffer := bytes.NewBuffer([]byte{}) buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer) c.Render(PNG, buffer)
assert.True(true, "Render needs to finish.") testutil.AssertTrue(t, true, "Render needs to finish.")
} }
func TestChartRegressionBadRangesByUser(t *testing.T) { func TestChartRegressionBadRangesByUser(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
YAxis: YAxis{ YAxis: YAxis{
@ -392,43 +396,43 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
}, },
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
}, },
}, },
} }
buffer := bytes.NewBuffer([]byte{}) buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer) c.Render(PNG, buffer)
assert.True(true, "Render needs to finish.") testutil.AssertTrue(t, true, "Render needs to finish.")
} }
func TestChartValidatesSeries(t *testing.T) { func TestChartValidatesSeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
}, },
}, },
} }
assert.Nil(c.validateSeries()) testutil.AssertNil(t, c.validateSeries())
c = Chart{ c = Chart{
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
}, },
}, },
} }
assert.NotNil(c.validateSeries()) testutil.AssertNotNil(t, c.validateSeries())
} }
func TestChartCheckRanges(t *testing.T) { func TestChartCheckRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Series: []Series{ Series: []Series{
@ -440,27 +444,11 @@ func TestChartCheckRanges(t *testing.T) {
} }
xr, yr, yra := c.getRanges() xr, yr, yra := c.getRanges()
assert.Nil(c.checkRanges(xr, yr, yra)) testutil.AssertNil(t, 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) { func TestChartCheckRangesWithRanges(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
XAxis: XAxis{ XAxis: XAxis{
@ -484,7 +472,7 @@ func TestChartCheckRangesWithRanges(t *testing.T) {
} }
xr, yr, yra := c.getRanges() xr, yr, yra := c.getRanges()
assert.Nil(c.checkRanges(xr, yr, yra)) testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
} }
func at(i image.Image, x, y int) drawing.Color { func at(i image.Image, x, y int) drawing.Color {
@ -492,85 +480,115 @@ func at(i image.Image, x, y int) drawing.Color {
} }
func TestChartE2ELine(t *testing.T) { func TestChartE2ELine(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
c := Chart{ c := Chart{
Height: 50, Height: 50,
Width: 50, Width: 50,
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Canvas: Style{ Canvas: Style{
Padding: Box{IsSet: true}, Padding: BoxZero,
}, },
Background: Style{ Background: Style{
Padding: Box{IsSet: true}, Padding: BoxZero,
}, },
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: seq.RangeWithStep(0, 4, 1), XValues: LinearRangeWithStep(0, 4, 1),
YValues: seq.RangeWithStep(0, 4, 1), YValues: LinearRangeWithStep(0, 4, 1),
}, },
}, },
} }
var buffer = &bytes.Buffer{} var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer) err := c.Render(PNG, buffer)
assert.Nil(err) testutil.AssertNil(t, err)
// do color tests ... // do color tests ...
i, err := png.Decode(buffer) i, err := png.Decode(buffer)
assert.Nil(err) testutil.AssertNil(t, err)
// test the bottom and top of the line // test the bottom and top of the line
assert.Equal(drawing.ColorWhite, at(i, 0, 0)) testutil.AssertEqual(t, drawing.ColorWhite, at(i, 0, 0))
assert.Equal(drawing.ColorWhite, at(i, 49, 49)) testutil.AssertEqual(t, drawing.ColorWhite, at(i, 49, 49))
// test a line mid point // test a line mid point
defaultSeriesColor := GetDefaultColor(0) defaultSeriesColor := GetDefaultColor(0)
assert.Equal(defaultSeriesColor, at(i, 0, 49)) testutil.AssertEqual(t, defaultSeriesColor, at(i, 0, 49))
assert.Equal(defaultSeriesColor, at(i, 49, 0)) testutil.AssertEqual(t, defaultSeriesColor, at(i, 49, 0))
assert.Equal(drawing.ColorFromHex("bddbf6"), at(i, 24, 24)) testutil.AssertEqual(t, drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
} }
func TestChartE2ELineWithFill(t *testing.T) { func TestChartE2ELineWithFill(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
logBuffer := new(bytes.Buffer)
c := Chart{ c := Chart{
Height: 50, Height: 50,
Width: 50, Width: 50,
Canvas: Style{ Canvas: Style{
Padding: Box{IsSet: true}, Padding: BoxZero,
}, },
Background: Style{ Background: Style{
Padding: Box{IsSet: true}, Padding: BoxZero,
}, },
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
Style: Style{ Style: Style{
Show: true,
StrokeColor: drawing.ColorBlue, StrokeColor: drawing.ColorBlue,
FillColor: drawing.ColorRed, FillColor: drawing.ColorRed,
}, },
XValues: seq.RangeWithStep(0, 4, 1), XValues: LinearRangeWithStep(0, 4, 1),
YValues: seq.RangeWithStep(0, 4, 1), YValues: LinearRangeWithStep(0, 4, 1),
},
},
Log: NewLogger(OptLoggerStdout(logBuffer), OptLoggerStderr(logBuffer)),
}
testutil.AssertEqual(t, 5, len(c.Series[0].(ContinuousSeries).XValues))
testutil.AssertEqual(t, 5, len(c.Series[0].(ContinuousSeries).YValues))
var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer)
testutil.AssertNil(t, err)
i, err := png.Decode(buffer)
testutil.AssertNil(t, err)
// test the bottom and top of the line
testutil.AssertEqual(t, drawing.ColorWhite, at(i, 0, 0))
testutil.AssertEqual(t, drawing.ColorRed, at(i, 49, 49))
// test a line mid point
defaultSeriesColor := drawing.ColorBlue
testutil.AssertEqual(t, defaultSeriesColor, at(i, 0, 49))
testutil.AssertEqual(t, defaultSeriesColor, at(i, 49, 0))
}
func Test_Chart_cve(t *testing.T) {
poc := StackedBarChart{
Title: "poc",
Bars: []StackedBar{
{
Name: "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
Values: []Value{
{Value: 1, Label: "infinite"},
{Value: 1, Label: "loop"},
},
}, },
}, },
} }
var buffer = &bytes.Buffer{} var imgContent bytes.Buffer
err := c.Render(PNG, buffer) err := poc.Render(PNG, &imgContent)
assert.Nil(err) testutil.AssertNotNil(t, 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))
} }

148
cmd/chart/main.go Normal file
View file

@ -0,0 +1,148 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"git.smarteching.com/zeni/go-chart/v2"
)
var (
outputPath = flag.String("output", "", "The output file")
inputFormat = flag.String("format", "csv", "The input format, either 'csv' or 'tsv' (defaults to 'csv')")
inputPath = flag.String("f", "", "The input file")
reverse = flag.Bool("reverse", false, "If we should reverse the inputs")
hideLegend = flag.Bool("hide-legend", false, "If we should omit the chart legend")
hideSMA = flag.Bool("hide-sma", false, "If we should omit simple moving average")
hideLinreg = flag.Bool("hide-linreg", false, "If we should omit linear regressions")
hideLastValues = flag.Bool("hide-last-values", false, "If we should omit last values")
)
func main() {
flag.Parse()
log := chart.NewLogger()
var rawData []byte
var err error
if *inputPath != "" {
if *inputPath == "-" {
rawData, err = ioutil.ReadAll(os.Stdin)
if err != nil {
log.FatalErr(err)
}
} else {
rawData, err = ioutil.ReadFile(*inputPath)
if err != nil {
log.FatalErr(err)
}
}
} else if len(flag.Args()) > 0 {
rawData = []byte(flag.Args()[0])
} else {
flag.Usage()
os.Exit(1)
}
var parts []string
switch *inputFormat {
case "csv":
parts = chart.SplitCSV(string(rawData))
case "tsv":
parts = strings.Split(string(rawData), "\t")
default:
log.FatalErr(fmt.Errorf("invalid format; must be 'csv' or 'tsv'"))
}
yvalues, err := chart.ParseFloats(parts...)
if err != nil {
log.FatalErr(err)
}
if *reverse {
yvalues = chart.ValueSequence(yvalues...).Reverse().Values()
}
var series []chart.Series
mainSeries := chart.ContinuousSeries{
Name: "Values",
XValues: chart.LinearRange(1, float64(len(yvalues))),
YValues: yvalues,
}
series = append(series, mainSeries)
smaSeries := &chart.SMASeries{
Name: "SMA",
Style: chart.Style{
Hidden: *hideSMA,
StrokeColor: chart.ColorRed,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
series = append(series, smaSeries)
linRegSeries := &chart.LinearRegressionSeries{
Name: "Values - Lin. Reg.",
Style: chart.Style{
Hidden: *hideLinreg,
},
InnerSeries: mainSeries,
}
series = append(series, linRegSeries)
mainLastValue := chart.LastValueAnnotationSeries(mainSeries)
mainLastValue.Style = chart.Style{
Hidden: *hideLastValues,
}
series = append(series, mainLastValue)
linregLastValue := chart.LastValueAnnotationSeries(linRegSeries)
linregLastValue.Style = chart.Style{
Hidden: (*hideLastValues || *hideLinreg),
}
series = append(series, linregLastValue)
smaLastValue := chart.LastValueAnnotationSeries(smaSeries)
smaLastValue.Style = chart.Style{
Hidden: (*hideLastValues || *hideSMA),
}
series = append(series, smaLastValue)
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 50,
},
},
Series: series,
}
if !*hideLegend {
graph.Elements = []chart.Renderable{chart.LegendThin(&graph)}
}
var output *os.File
if *outputPath != "" {
output, err = os.Create(*outputPath)
if err != nil {
log.FatalErr(err)
}
} else {
output, err = ioutil.TempFile("", "*.png")
if err != nil {
log.FatalErr(err)
}
}
if err := graph.Render(chart.PNG, output); err != nil {
log.FatalErr(err)
}
fmt.Fprintln(os.Stdout, output.Name())
os.Exit(0)
}

View file

@ -1,6 +1,6 @@
package chart package chart
import "github.com/wcharczuk/go-chart/drawing" import "git.smarteching.com/zeni/go-chart/v2/drawing"
var ( var (
// ColorWhite is white. // ColorWhite is white.

View file

@ -3,40 +3,39 @@ package chart
import ( import (
"testing" "testing"
assert "github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/seq"
) )
func TestConcatSeries(t *testing.T) { func TestConcatSeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
s1 := ContinuousSeries{ s1 := ContinuousSeries{
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
} }
s2 := ContinuousSeries{ s2 := ContinuousSeries{
XValues: seq.Range(11, 20.0), XValues: LinearRange(11, 20.0),
YValues: seq.Range(10.0, 1.0), YValues: LinearRange(10.0, 1.0),
} }
s3 := ContinuousSeries{ s3 := ContinuousSeries{
XValues: seq.Range(21, 30.0), XValues: LinearRange(21, 30.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
} }
cs := ConcatSeries([]Series{s1, s2, s3}) cs := ConcatSeries([]Series{s1, s2, s3})
assert.Equal(30, cs.Len()) testutil.AssertEqual(t, 30, cs.Len())
x0, y0 := cs.GetValue(0) x0, y0 := cs.GetValue(0)
assert.Equal(1.0, x0) testutil.AssertEqual(t, 1.0, x0)
assert.Equal(1.0, y0) testutil.AssertEqual(t, 1.0, y0)
xm, ym := cs.GetValue(19) xm, ym := cs.GetValue(19)
assert.Equal(20.0, xm) testutil.AssertEqual(t, 20.0, xm)
assert.Equal(1.0, ym) testutil.AssertEqual(t, 1.0, ym)
xn, yn := cs.GetValue(29) xn, yn := cs.GetValue(29)
assert.Equal(30.0, xn) testutil.AssertEqual(t, 30.0, xn)
assert.Equal(10.0, yn) testutil.AssertEqual(t, 10.0, yn)
} }

View file

@ -62,6 +62,9 @@ func (r *ContinuousRange) SetDomain(domain int) {
// String returns a simple string for the ContinuousRange. // String returns a simple string for the ContinuousRange.
func (r ContinuousRange) String() string { func (r ContinuousRange) String() string {
if r.GetDelta() == 0 {
return "ContinuousRange [empty]"
}
return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
} }

View file

@ -3,21 +3,20 @@ package chart
import ( import (
"testing" "testing"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/util"
) )
func TestRangeTranslate(t *testing.T) { func TestRangeTranslate(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} 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 := ContinuousRange{Domain: 1000}
r.Min, r.Max = util.Math.MinAndMax(values...) r.Min, r.Max = MinMax(values...)
// delta = ~7.0 // delta = ~7.0
// value = ~5.0 // value = ~5.0
// domain = ~1000 // domain = ~1000
// 5/8 * 1000 ~= // 5/8 * 1000 ~=
assert.Equal(0, r.Translate(1.0)) testutil.AssertEqual(t, 0, r.Translate(1.0))
assert.Equal(1000, r.Translate(8.0)) testutil.AssertEqual(t, 1000, r.Translate(8.0))
assert.Equal(572, r.Translate(5.0)) testutil.AssertEqual(t, 572, r.Translate(5.0))
} }

View file

@ -2,6 +2,13 @@ package chart
import "fmt" import "fmt"
// Interface Assertions.
var (
_ Series = (*ContinuousSeries)(nil)
_ FirstValuesProvider = (*ContinuousSeries)(nil)
_ LastValuesProvider = (*ContinuousSeries)(nil)
)
// ContinuousSeries represents a line on a chart. // ContinuousSeries represents a line on a chart.
type ContinuousSeries struct { type ContinuousSeries struct {
Name string Name string
@ -36,6 +43,11 @@ func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
return cs.XValues[index], cs.YValues[index] 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. // GetLastValues gets the last x,y values.
func (cs ContinuousSeries) GetLastValues() (float64, float64) { func (cs ContinuousSeries) GetLastValues() (float64, float64) {
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1] return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
@ -70,11 +82,15 @@ func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang
// Validate validates the series. // Validate validates the series.
func (cs ContinuousSeries) Validate() error { func (cs ContinuousSeries) Validate() error {
if len(cs.XValues) == 0 { if len(cs.XValues) == 0 {
return fmt.Errorf("continuous series must have xvalues set") return fmt.Errorf("continuous series; must have xvalues set")
} }
if len(cs.YValues) == 0 { if len(cs.YValues) == 0 {
return fmt.Errorf("continuous series must have yvalues set") return fmt.Errorf("continuous series; must have yvalues set")
}
if len(cs.XValues) != len(cs.YValues) {
return fmt.Errorf("continuous series; must have same length xvalues as yvalues")
} }
return nil return nil
} }

View file

@ -4,36 +4,35 @@ import (
"fmt" "fmt"
"testing" "testing"
assert "github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/seq"
) )
func TestContinuousSeries(t *testing.T) { func TestContinuousSeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
cs := ContinuousSeries{ cs := ContinuousSeries{
Name: "Test Series", Name: "Test Series",
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
} }
assert.Equal("Test Series", cs.GetName()) testutil.AssertEqual(t, "Test Series", cs.GetName())
assert.Equal(10, cs.Len()) testutil.AssertEqual(t, 10, cs.Len())
x0, y0 := cs.GetValues(0) x0, y0 := cs.GetValues(0)
assert.Equal(1.0, x0) testutil.AssertEqual(t, 1.0, x0)
assert.Equal(1.0, y0) testutil.AssertEqual(t, 1.0, y0)
xn, yn := cs.GetValues(9) xn, yn := cs.GetValues(9)
assert.Equal(10.0, xn) testutil.AssertEqual(t, 10.0, xn)
assert.Equal(10.0, yn) testutil.AssertEqual(t, 10.0, yn)
xn, yn = cs.GetLastValues() xn, yn = cs.GetLastValues()
assert.Equal(10.0, xn) testutil.AssertEqual(t, 10.0, xn)
assert.Equal(10.0, yn) testutil.AssertEqual(t, 10.0, yn)
} }
func TestContinuousSeriesValueFormatter(t *testing.T) { func TestContinuousSeriesValueFormatter(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
cs := ContinuousSeries{ cs := ContinuousSeries{
XValueFormatter: func(v interface{}) string { XValueFormatter: func(v interface{}) string {
@ -45,29 +44,29 @@ func TestContinuousSeriesValueFormatter(t *testing.T) {
} }
xf, yf := cs.GetValueFormatters() xf, yf := cs.GetValueFormatters()
assert.Equal("0.100000 foo", xf(0.1)) testutil.AssertEqual(t, "0.100000 foo", xf(0.1))
assert.Equal("0.100000 bar", yf(0.1)) testutil.AssertEqual(t, "0.100000 bar", yf(0.1))
} }
func TestContinuousSeriesValidate(t *testing.T) { func TestContinuousSeriesValidate(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
cs := ContinuousSeries{ cs := ContinuousSeries{
Name: "Test Series", Name: "Test Series",
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
} }
assert.Nil(cs.Validate()) testutil.AssertNil(t, cs.Validate())
cs = ContinuousSeries{ cs = ContinuousSeries{
Name: "Test Series", Name: "Test Series",
XValues: seq.Range(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
} }
assert.NotNil(cs.Validate()) testutil.AssertNotNil(t, cs.Validate())
cs = ContinuousSeries{ cs = ContinuousSeries{
Name: "Test Series", Name: "Test Series",
YValues: seq.Range(1.0, 10.0), YValues: LinearRange(1.0, 10.0),
} }
assert.NotNil(cs.Validate()) testutil.AssertNotNil(t, cs.Validate())
} }

315
donut_chart.go Normal file
View file

@ -0,0 +1,315 @@
package chart
import (
"errors"
"fmt"
"io"
"github.com/golang/freetype/truetype"
)
// DonutChart is a chart that draws sections of a circle based on percentages with an hole.
type DonutChart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
Background Style
Canvas Style
SliceStyle Style
Font *truetype.Font
defaultFont *truetype.Font
Values []Value
Elements []Renderable
}
// GetDPI returns the dpi for the chart.
func (pc DonutChart) GetDPI(defaults ...float64) float64 {
if pc.DPI == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultDPI
}
return pc.DPI
}
// GetFont returns the text font.
func (pc DonutChart) GetFont() *truetype.Font {
if pc.Font == nil {
return pc.defaultFont
}
return pc.Font
}
// GetWidth returns the chart width or the default value.
func (pc DonutChart) GetWidth() int {
if pc.Width == 0 {
return DefaultChartWidth
}
return pc.Width
}
// GetHeight returns the chart height or the default value.
func (pc DonutChart) GetHeight() int {
if pc.Height == 0 {
return DefaultChartWidth
}
return pc.Height
}
// Render renders the chart with the given renderer to the given io.Writer.
func (pc DonutChart) Render(rp RendererProvider, w io.Writer) error {
if len(pc.Values) == 0 {
return errors.New("please provide at least one value")
}
r, err := rp(pc.GetWidth(), pc.GetHeight())
if err != nil {
return err
}
if pc.Font == nil {
defaultFont, err := GetDefaultFont()
if err != nil {
return err
}
pc.defaultFont = defaultFont
}
r.SetDPI(pc.GetDPI(DefaultDPI))
canvasBox := pc.getDefaultCanvasBox()
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
pc.drawBackground(r)
pc.drawCanvas(r, canvasBox)
finalValues, err := pc.finalizeValues(pc.Values)
if err != nil {
return err
}
pc.drawSlices(r, canvasBox, finalValues)
pc.drawTitle(r)
for _, a := range pc.Elements {
a(r, canvasBox, pc.styleDefaultsElements())
}
return r.Save(w)
}
func (pc DonutChart) drawBackground(r Renderer) {
Draw.Box(r, Box{
Right: pc.GetWidth(),
Bottom: pc.GetHeight(),
}, pc.getBackgroundStyle())
}
func (pc DonutChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, pc.getCanvasStyle())
}
func (pc DonutChart) drawTitle(r Renderer) {
if len(pc.Title) > 0 && !pc.TitleStyle.Hidden {
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
}
}
func (pc DonutChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
cx, cy := canvasBox.Center()
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
radius := float64(diameter>>1) / 1.1
labelRadius := (radius * 2.83) / 3.0
// draw the donut slices
var rads, delta, delta2, total float64
var lx, ly int
if len(values) == 1 {
pc.styleDonutChartValue(0).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.Circle(radius, cx, cy)
} else {
for index, v := range values {
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy)
rads = PercentToRadians(total)
delta = PercentToRadians(v.Value)
r.ArcTo(cx, cy, (radius / 1.25), (radius / 1.25), rads, delta)
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
total = total + v.Value
}
}
//making the donut hole
v := Value{Value: 100, Label: "center"}
styletemp := pc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite, StrokeWidth: 4.0, FillColor: ColorWhite, FontColor: ColorWhite, //Font: pc.GetFont(),//FontSize: pc.getScaledFontSize(),
})
v.Style.InheritFrom(styletemp).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.ArcTo(cx, cy, (radius / 3.5), (radius / 3.5), DegreesToRadians(0), DegreesToRadians(359))
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
// draw the labels
total = 0
for index, v := range values {
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2)
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
ly = ly + (tb.Height() >> 1)
r.Text(v.Label, lx, ly)
}
total = total + v.Value
}
}
func (pc DonutChart) finalizeValues(values []Value) ([]Value, error) {
finalValues := Values(values).Normalize()
if len(finalValues) == 0 {
return nil, fmt.Errorf("donut chart must contain at least (1) non-zero value")
}
return finalValues, nil
}
func (pc DonutChart) getDefaultCanvasBox() Box {
return pc.Box()
}
func (pc DonutChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
square := Box{
Right: circleDiameter,
Bottom: circleDiameter,
}
return canvasBox.Fit(square)
}
func (pc DonutChart) getBackgroundStyle() Style {
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
}
func (pc DonutChart) getCanvasStyle() Style {
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
}
func (pc DonutChart) styleDefaultsCanvas() Style {
return Style{
FillColor: pc.GetColorPalette().CanvasColor(),
StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc DonutChart) styleDefaultsDonutChartValue() Style {
return Style{
StrokeColor: pc.GetColorPalette().TextColor(),
StrokeWidth: 4.0,
FillColor: pc.GetColorPalette().TextColor(),
}
}
func (pc DonutChart) styleDonutChartValue(index int) Style {
return pc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite,
StrokeWidth: 4.0,
FillColor: pc.GetColorPalette().GetSeriesColor(index),
FontSize: pc.getScaledFontSize(),
FontColor: pc.GetColorPalette().TextColor(),
Font: pc.GetFont(),
})
}
func (pc DonutChart) getScaledFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 {
return 48.0
} else if effectiveDimension >= 1024 {
return 24.0
} else if effectiveDimension > 512 {
return 18.0
} else if effectiveDimension > 256 {
return 12.0
}
return 10.0
}
func (pc DonutChart) styleDefaultsBackground() Style {
return Style{
FillColor: pc.GetColorPalette().BackgroundColor(),
StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc DonutChart) styleDefaultsElements() Style {
return Style{
Font: pc.GetFont(),
}
}
func (pc DonutChart) styleDefaultsTitle() Style {
return pc.TitleStyle.InheritFrom(Style{
FontColor: pc.GetColorPalette().TextColor(),
Font: pc.GetFont(),
FontSize: pc.getTitleFontSize(),
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
})
}
func (pc DonutChart) getTitleFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 {
return 48
} else if effectiveDimension >= 1024 {
return 24
} else if effectiveDimension >= 512 {
return 18
} else if effectiveDimension >= 256 {
return 12
}
return 10
}
// GetColorPalette returns the color palette for the chart.
func (pc DonutChart) GetColorPalette() ColorPalette {
if pc.ColorPalette != nil {
return pc.ColorPalette
}
return AlternateColorPalette
}
// Box returns the chart bounds as a box.
func (pc DonutChart) Box() Box {
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
Right: pc.GetWidth() - dpr,
Bottom: pc.GetHeight() - dpb,
}
}

69
donut_chart_test.go Normal file
View file

@ -0,0 +1,69 @@
package chart
import (
"bytes"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestDonutChart(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 10, Label: "Blue"},
{Value: 9, Label: "Green"},
{Value: 8, Label: "Gray"},
{Value: 7, Label: "Orange"},
{Value: 6, Label: "HEANG"},
{Value: 5, Label: "??"},
{Value: 2, Label: "!!"},
},
}
b := bytes.NewBuffer([]byte{})
pie.Render(PNG, b)
testutil.AssertNotZero(t, b.Len())
}
func TestDonutChartDropsZeroValues(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 5, Label: "Blue"},
{Value: 5, Label: "Green"},
{Value: 0, Label: "Gray"},
},
}
b := bytes.NewBuffer([]byte{})
err := pie.Render(PNG, b)
testutil.AssertNil(t, err)
}
func TestDonutChartAllZeroValues(t *testing.T) {
// replaced new assertions helper
pie := DonutChart{
Canvas: Style{
FillColor: ColorLightGray,
},
Values: []Value{
{Value: 0, Label: "Blue"},
{Value: 0, Label: "Green"},
{Value: 0, Label: "Gray"},
},
}
b := bytes.NewBuffer([]byte{})
err := pie.Render(PNG, b)
testutil.AssertNotNil(t, err)
}

12
draw.go
View file

@ -2,8 +2,6 @@ package chart
import ( import (
"math" "math"
util "github.com/wcharczuk/go-chart/util"
) )
var ( var (
@ -40,8 +38,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
y = cb - yrange.Translate(vy) y = cb - yrange.Translate(vy)
r.LineTo(x, y) r.LineTo(x, y)
} }
r.LineTo(x, util.Math.MinInt(cb, cb-yv0)) r.LineTo(x, MinInt(cb, cb-yv0))
r.LineTo(x0, util.Math.MinInt(cb, cb-yv0)) r.LineTo(x0, MinInt(cb, cb-yv0))
r.LineTo(x0, y0) r.LineTo(x0, y0)
r.Fill() r.Fill()
} }
@ -298,8 +296,10 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
switch style.GetTextVerticalAlign() { switch style.GetTextVerticalAlign() {
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
y = y - linesBox.Height() y = y - linesBox.Height()
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline: case TextVerticalAlignMiddle:
y = (y - linesBox.Height()) >> 1 y = y + (box.Height() >> 1) - (linesBox.Height() >> 1)
case TextVerticalAlignMiddleBaseline:
y = y + (box.Height() >> 1) - linesBox.Height()
} }
var tx, ty int var tx, ty int

View file

@ -2,27 +2,46 @@ package drawing
import ( import (
"fmt" "fmt"
"regexp"
"strconv" "strconv"
"strings"
) )
// Basic Colors from:
// https://www.w3.org/wiki/CSS/Properties/color/keywords
var ( var (
// ColorTransparent is a fully transparent color. // ColorTransparent is a fully transparent color.
ColorTransparent = Color{} ColorTransparent = Color{R: 255, G: 255, B: 255, A: 0}
// ColorWhite is white. // ColorWhite is white.
ColorWhite = Color{R: 255, G: 255, B: 255, A: 255} ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
// ColorBlack is black. // ColorBlack is black.
ColorBlack = Color{R: 0, G: 0, B: 0, A: 255} ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
// ColorRed is red. // ColorRed is red.
ColorRed = Color{R: 255, G: 0, B: 0, A: 255} ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
// ColorGreen is green. // ColorGreen is green.
ColorGreen = Color{R: 0, G: 255, B: 0, A: 255} ColorGreen = Color{R: 0, G: 128, B: 0, A: 255}
// ColorBlue is blue. // ColorBlue is blue.
ColorBlue = Color{R: 0, G: 0, B: 255, A: 255} ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
// ColorSilver is a known color.
ColorSilver = Color{R: 192, G: 192, B: 192, A: 255}
// ColorMaroon is a known color.
ColorMaroon = Color{R: 128, G: 0, B: 0, A: 255}
// ColorPurple is a known color.
ColorPurple = Color{R: 128, G: 0, B: 128, A: 255}
// ColorFuchsia is a known color.
ColorFuchsia = Color{R: 255, G: 0, B: 255, A: 255}
// ColorLime is a known color.
ColorLime = Color{R: 0, G: 255, B: 0, A: 255}
// ColorOlive is a known color.
ColorOlive = Color{R: 128, G: 128, B: 0, A: 255}
// ColorYellow is a known color.
ColorYellow = Color{R: 255, G: 255, B: 0, A: 255}
// ColorNavy is a known color.
ColorNavy = Color{R: 0, G: 0, B: 128, A: 255}
// ColorTeal is a known color.
ColorTeal = Color{R: 0, G: 128, B: 128, A: 255}
// ColorAqua is a known color.
ColorAqua = Color{R: 0, G: 255, B: 255, A: 255}
) )
func parseHex(hex string) uint8 { func parseHex(hex string) uint8 {
@ -30,8 +49,97 @@ func parseHex(hex string) uint8 {
return uint8(v) return uint8(v)
} }
// ParseColor parses a color from a string.
func ParseColor(rawColor string) Color {
if strings.HasPrefix(rawColor, "rgba") {
return ColorFromRGBA(rawColor)
}
if strings.HasPrefix(rawColor, "rgb") {
return ColorFromRGB(rawColor)
}
if strings.HasPrefix(rawColor, "#") {
return ColorFromHex(rawColor)
}
return ColorFromKnown(rawColor)
}
var rgbaexpr = regexp.MustCompile(`rgba\((?P<R>.+),(?P<G>.+),(?P<B>.+),(?P<A>.+)\)`)
// ColorFromRGBA returns a color from an `rgba()` css function.
func ColorFromRGBA(rgba string) (output Color) {
values := rgbaexpr.FindStringSubmatch(rgba)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
case "A":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseFloat(value, 32)
if parsed > 1 {
parsed = 1
} else if parsed < 0 {
parsed = 0
}
output.A = uint8(parsed * 255)
}
}
return
}
var rgbexpr = regexp.MustCompile(`rgb\((?P<R>.+),(?P<G>.+),(?P<B>.+)\)`)
// ColorFromRGB returns a color from an `rgb()` css function.
func ColorFromRGB(rgb string) (output Color) {
output.A = 255
values := rgbexpr.FindStringSubmatch(rgb)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
}
}
return
}
// ColorFromHex returns a color from a css hex code. // ColorFromHex returns a color from a css hex code.
//
// NOTE: it will trim a leading '#' character if present.
func ColorFromHex(hex string) Color { func ColorFromHex(hex string) Color {
if strings.HasPrefix(hex, "#") {
hex = strings.TrimPrefix(hex, "#")
}
var c Color var c Color
if len(hex) == 3 { if len(hex) == 3 {
c.R = parseHex(string(hex[0])) * 0x11 c.R = parseHex(string(hex[0])) * 0x11
@ -46,6 +154,46 @@ func ColorFromHex(hex string) Color {
return c return c
} }
// ColorFromKnown returns an internal color from a known (basic) color name.
func ColorFromKnown(known string) Color {
switch strings.ToLower(known) {
case "transparent":
return ColorTransparent
case "white":
return ColorWhite
case "black":
return ColorBlack
case "red":
return ColorRed
case "blue":
return ColorBlue
case "green":
return ColorGreen
case "silver":
return ColorSilver
case "maroon":
return ColorMaroon
case "purple":
return ColorPurple
case "fuchsia":
return ColorFuchsia
case "lime":
return ColorLime
case "olive":
return ColorOlive
case "yellow":
return ColorYellow
case "navy":
return ColorNavy
case "teal":
return ColorTeal
case "aqua":
return ColorAqua
default:
return Color{}
}
}
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values. // ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color { func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
fa := float64(a) / 255.0 fa := float64(a) / 255.0

View file

@ -1,53 +1,114 @@
package drawing package drawing
import ( import (
"fmt"
"testing" "testing"
"image/color" "image/color"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
) )
func TestColorFromHex(t *testing.T) { func TestColorFromHex(t *testing.T) {
assert := assert.New(t)
white := ColorFromHex("FFFFFF") white := ColorFromHex("FFFFFF")
assert.Equal(ColorWhite, white) testutil.AssertEqual(t, ColorWhite, white)
shortWhite := ColorFromHex("FFF") shortWhite := ColorFromHex("FFF")
assert.Equal(ColorWhite, shortWhite) testutil.AssertEqual(t, ColorWhite, shortWhite)
black := ColorFromHex("000000") black := ColorFromHex("000000")
assert.Equal(ColorBlack, black) testutil.AssertEqual(t, ColorBlack, black)
shortBlack := ColorFromHex("000") shortBlack := ColorFromHex("000")
assert.Equal(ColorBlack, shortBlack) testutil.AssertEqual(t, ColorBlack, shortBlack)
red := ColorFromHex("FF0000") red := ColorFromHex("FF0000")
assert.Equal(ColorRed, red) testutil.AssertEqual(t, ColorRed, red)
shortRed := ColorFromHex("F00") shortRed := ColorFromHex("F00")
assert.Equal(ColorRed, shortRed) testutil.AssertEqual(t, ColorRed, shortRed)
green := ColorFromHex("00FF00") green := ColorFromHex("008000")
assert.Equal(ColorGreen, green) testutil.AssertEqual(t, ColorGreen, green)
shortGreen := ColorFromHex("0F0") // shortGreen := ColorFromHex("0F0")
assert.Equal(ColorGreen, shortGreen) // testutil.AssertEqual(t, ColorGreen, shortGreen)
blue := ColorFromHex("0000FF") blue := ColorFromHex("0000FF")
assert.Equal(ColorBlue, blue) testutil.AssertEqual(t, ColorBlue, blue)
shortBlue := ColorFromHex("00F") shortBlue := ColorFromHex("00F")
assert.Equal(ColorBlue, shortBlue) testutil.AssertEqual(t, ColorBlue, shortBlue)
}
func TestColorFromHex_handlesHash(t *testing.T) {
withHash := ColorFromHex("#FF0000")
testutil.AssertEqual(t, ColorRed, withHash)
withoutHash := ColorFromHex("#FF0000")
testutil.AssertEqual(t, ColorRed, withoutHash)
} }
func TestColorFromAlphaMixedRGBA(t *testing.T) { func TestColorFromAlphaMixedRGBA(t *testing.T) {
assert := assert.New(t)
black := ColorFromAlphaMixedRGBA(color.Black.RGBA()) black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
assert.True(black.Equals(ColorBlack), black.String()) testutil.AssertTrue(t, black.Equals(ColorBlack), black.String())
white := ColorFromAlphaMixedRGBA(color.White.RGBA()) white := ColorFromAlphaMixedRGBA(color.White.RGBA())
assert.True(white.Equals(ColorWhite), white.String()) testutil.AssertTrue(t, white.Equals(ColorWhite), white.String())
}
func Test_ColorFromRGBA(t *testing.T) {
value := "rgba(192, 192, 192, 1.0)"
parsed := ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
value = "rgba(192,192,192,1.0)"
parsed = ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
value = "rgba(192,192,192,1.5)"
parsed = ColorFromRGBA(value)
testutil.AssertEqual(t, ColorSilver, parsed)
}
func TestParseColor(t *testing.T) {
testCases := [...]struct {
Input string
Expected Color
}{
{"", Color{}},
{"white", ColorWhite},
{"WHITE", ColorWhite}, // caps!
{"black", ColorBlack},
{"red", ColorRed},
{"green", ColorGreen},
{"blue", ColorBlue},
{"silver", ColorSilver},
{"maroon", ColorMaroon},
{"purple", ColorPurple},
{"fuchsia", ColorFuchsia},
{"lime", ColorLime},
{"olive", ColorOlive},
{"yellow", ColorYellow},
{"navy", ColorNavy},
{"teal", ColorTeal},
{"aqua", ColorAqua},
{"rgba(192, 192, 192, 1.0)", ColorSilver},
{"rgba(192,192,192,1.0)", ColorSilver},
{"rgb(192, 192, 192)", ColorSilver},
{"rgb(192,192,192)", ColorSilver},
{"#FF0000", ColorRed},
{"#008000", ColorGreen},
{"#0000FF", ColorBlue},
{"#F00", ColorRed},
{"#080", Color{0, 136, 0, 255}},
{"#00F", ColorBlue},
}
for index, tc := range testCases {
actual := ParseColor(tc.Input)
testutil.AssertEqual(t, tc.Expected, actual, fmt.Sprintf("test case: %d -> %s", index, tc.Input))
}
} }

View file

@ -3,7 +3,7 @@ package drawing
import ( import (
"testing" "testing"
assert "github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
) )
type point struct { type point struct {
@ -23,7 +23,7 @@ func (ml mockLine) Len() int {
} }
func TestTraceQuad(t *testing.T) { func TestTraceQuad(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
// Quad // Quad
// x1, y1, cpx1, cpy2, x2, y2 float64 // x1, y1, cpx1, cpy2, x2, y2 float64
@ -31,5 +31,5 @@ func TestTraceQuad(t *testing.T) {
quad := []float64{10, 20, 20, 20, 20, 10} quad := []float64{10, 20, 20, 20, 20, 10}
liner := &mockLine{} liner := &mockLine{}
TraceQuad(liner, quad, 0.5) TraceQuad(liner, quad, 0.5)
assert.NotZero(liner.Len()) testutil.AssertNotZero(t, liner.Len())
} }

View file

@ -7,6 +7,13 @@ const (
DefaultEMAPeriod = 12 DefaultEMAPeriod = 12
) )
// Interface Assertions.
var (
_ Series = (*EMASeries)(nil)
_ FirstValuesProvider = (*EMASeries)(nil)
_ LastValuesProvider = (*EMASeries)(nil)
)
// EMASeries is a computed series. // EMASeries is a computed series.
type EMASeries struct { type EMASeries struct {
Name string Name string
@ -66,6 +73,19 @@ func (ema *EMASeries) GetValues(index int) (x, y float64) {
return 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, // GetLastValues computes the last moving average value but walking back window size samples,
// and recomputing the last moving average chunk. // and recomputing the last moving average chunk.
func (ema *EMASeries) GetLastValues() (x, y float64) { func (ema *EMASeries) GetLastValues() (x, y float64) {

View file

@ -3,12 +3,11 @@ package chart
import ( import (
"testing" "testing"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/testutil"
"github.com/wcharczuk/go-chart/seq"
) )
var ( var (
emaXValues = seq.Range(1.0, 50.0) emaXValues = LinearRange(1.0, 50.0)
emaYValues = []float64{ 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,
@ -74,13 +73,13 @@ var (
) )
func TestEMASeries(t *testing.T) { func TestEMASeries(t *testing.T) {
assert := assert.New(t) // replaced new assertions helper
mockSeries := mockValuesProvider{ mockSeries := mockValuesProvider{
emaXValues, emaXValues,
emaYValues, emaYValues,
} }
assert.Equal(50, mockSeries.Len()) testutil.AssertEqual(t, 50, mockSeries.Len())
ema := &EMASeries{ ema := &EMASeries{
InnerSeries: mockSeries, InnerSeries: mockSeries,
@ -88,7 +87,7 @@ func TestEMASeries(t *testing.T) {
} }
sig := ema.GetSigma() sig := ema.GetSigma()
assert.Equal(2.0/(26.0+1), sig) testutil.AssertEqual(t, 2.0/(26.0+1), sig)
var yvalues []float64 var yvalues []float64
for x := 0; x < ema.Len(); x++ { for x := 0; x < ema.Len(); x++ {
@ -97,10 +96,10 @@ func TestEMASeries(t *testing.T) {
} }
for index, yv := range yvalues { for index, yv := range yvalues {
assert.InDelta(yv, emaExpected[index], emaDelta) testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta)
} }
lvx, lvy := ema.GetLastValues() lvx, lvy := ema.GetLastValues()
assert.Equal(50.0, lvx) testutil.AssertEqual(t, 50.0, lvx)
assert.InDelta(lvy, emaExpected[49], emaDelta) testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta)
} }

View file

@ -1,13 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we add an `Annotation` series, which is a special type of series that In this example we add an `Annotation` series, which is a special type of series that
draws annotation labels at given X and Y values (as translated by their respective ranges). draws annotation labels at given X and Y values (as translated by their respective ranges).
@ -37,11 +38,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -14,20 +16,9 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
*/ */
graph := chart.Chart{ graph := chart.Chart{
XAxis: chart.XAxis{
Name: "The XAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
YAxis: chart.YAxis{
Name: "The YAxis",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
},
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Style: chart.Style{ Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64), FillColor: chart.GetDefaultColor(0).WithAlpha(64),
}, },
@ -37,11 +28,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

BIN
examples/axes/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -15,19 +17,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{ graph := chart.Chart{
XAxis: chart.XAxis{ XAxis: chart.XAxis{
Style: chart.Style{ Name: "The XAxis",
Show: true, //enables / displays the x-axis
},
}, },
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Style: chart.Style{ Name: "The YAxis",
Show: true, //enables / displays the y-axis
},
}, },
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Style: chart.Style{ Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64), FillColor: chart.GetDefaultColor(0).WithAlpha(64),
}, },
@ -37,11 +34,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,35 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
graph := chart.BarChart{
Title: "Test Bar Chart",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
BarWidth: 60,
Bars: []chart.Value{
{Value: 5.25, Label: "Blue"},
{Value: 4.88, Label: "Green"},
{Value: 4.74, Label: "Gray"},
{Value: 3.22, Label: "Orange"},
{Value: 3, Label: "Test"},
{Value: 2.27, Label: "??"},
{Value: 1, Label: "!!"},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,62 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func main() {
profitStyle := chart.Style{
FillColor: drawing.ColorFromHex("13c158"),
StrokeColor: drawing.ColorFromHex("13c158"),
StrokeWidth: 0,
}
lossStyle := chart.Style{
FillColor: drawing.ColorFromHex("c11313"),
StrokeColor: drawing.ColorFromHex("c11313"),
StrokeWidth: 0,
}
sbc := chart.BarChart{
Title: "Bar Chart Using BaseValue",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
BarWidth: 60,
YAxis: chart.YAxis{
Ticks: []chart.Tick{
{Value: -4.0, Label: "-4"},
{Value: -2.0, Label: "-2"},
{Value: 0, Label: "0"},
{Value: 2.0, Label: "2"},
{Value: 4.0, Label: "4"},
{Value: 6.0, Label: "6"},
{Value: 8.0, Label: "8"},
{Value: 10.0, Label: "10"},
{Value: 12.0, Label: "12"},
},
},
UseBaseValue: true,
BaseValue: 0.0,
Bars: []chart.Value{
{Value: 10.0, Style: profitStyle, Label: "Profit"},
{Value: 12.0, Style: profitStyle, Label: "More Profit"},
{Value: 8.0, Style: profitStyle, Label: "Still Profit"},
{Value: -4.0, Style: lossStyle, Label: "Loss!"},
{Value: 3.0, Style: profitStyle, Label: "Phew Ok"},
{Value: -2.0, Style: lossStyle, Label: "Oh No!"},
},
}
f, _ := os.Create("output.png")
defer f.Close()
sbc.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,26 +1,26 @@
package main package main
//go:generate go run main.go
import ( import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"github.com/wcharczuk/go-chart" "git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func drawChart(res http.ResponseWriter, req *http.Request) {
sbc := chart.BarChart{ graph := chart.BarChart{
Height: 512, Title: "Test Bar Chart",
BarWidth: 60, Background: chart.Style{
XAxis: chart.Style{ Padding: chart.Box{
Show: true, Top: 40,
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
}, },
}, },
Height: 512,
BarWidth: 60,
Bars: []chart.Value{ Bars: []chart.Value{
{Value: 5.25, Label: "Blue"}, {Value: 5.25, Label: "Blue"},
{Value: 4.88, Label: "Green"}, {Value: 4.88, Label: "Green"},
@ -33,10 +33,11 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
} }
res.Header().Set("Content-Type", "image/png") res.Header().Set("Content-Type", "image/png")
err := sbc.Render(chart.PNG, res) err := graph.Render(chart.PNG, res)
if err != nil { if err != nil {
fmt.Printf("Error rendering chart: %v\n", err) fmt.Printf("Error rendering chart: %v\n", err)
} }
} }
func port() string { func port() string {

23
examples/basic/main.go Normal file
View file

@ -0,0 +1,23 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
graph := chart.Chart{
Series: []chart.Series{
chart.ContinuousSeries{
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

BIN
examples/basic/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,52 @@
package main
//go:generate go run main.go
import (
"fmt"
"math/rand"
"os"
"time"
"git.smarteching.com/zeni/go-chart/v2"
)
func random(min, max float64) float64 {
return rand.Float64()*(max-min) + min
}
func main() {
numValues := 1024
numSeries := 100
series := make([]chart.Series, numSeries)
for i := 0; i < numSeries; i++ {
xValues := make([]time.Time, numValues)
yValues := make([]float64, numValues)
for j := 0; j < numValues; j++ {
xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1)
yValues[j] = random(float64(-500), float64(500))
}
series[i] = chart.TimeSeries{
Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i),
XValues: xValues,
YValues: yValues,
}
}
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "Time",
},
YAxis: chart.YAxis{
Name: "Value",
},
Series: series,
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View file

@ -0,0 +1,59 @@
package main
import (
"fmt"
"log"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
)
// Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example
func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) {
res.Write([]byte(
"<!DOCTYPE html><html><head>" +
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/main.css\">" +
"</head>" +
"<body>"))
pie := chart.PieChart{
// Notes: * Setting ClassName will cause all other inline styles to be dropped!
// * The following type classes may be added additionally: stroke, fill, text
Background: chart.Style{ClassName: "background"},
Canvas: chart.Style{
ClassName: "canvas",
},
Width: 512,
Height: 512,
Values: []chart.Value{
{Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}},
{Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}},
{Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}},
},
}
err := pie.Render(chart.SVG, res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
res.Write([]byte("</body>"))
}
func css(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/css")
res.Write([]byte("svg .background { fill: white; }" +
"svg .canvas { fill: white; }" +
"svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" +
"svg .green.fill.stroke { fill: green; stroke: lightgreen; }" +
"svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" +
"svg .blue.text { fill: white; }" +
"svg .green.text { fill: white; }" +
"svg .gray.text { fill: white; }"))
}
func main() {
http.HandleFunc("/", inlineSVGWithClasses)
http.HandleFunc("/main.css", css)
log.Fatal(http.ListenAndServe(":8080", nil))
}

View file

@ -1,13 +1,15 @@
package main package main
//go:generate go run main.go
import ( import (
"fmt" "fmt"
"net/http" "os"
"github.com/wcharczuk/go-chart" "git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we use a custom `ValueFormatter` for the y axis, letting us specify how to format text of the y-axis ticks. In this example we use a custom `ValueFormatter` for the y axis, letting us specify how to format text of the y-axis ticks.
You can also do this for the x-axis, or the secondary y-axis. You can also do this for the x-axis, or the secondary y-axis.
@ -16,9 +18,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{ graph := chart.Chart{
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
ValueFormatter: func(v interface{}) string { ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat { if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%0.6f", vf) return fmt.Sprintf("%0.6f", vf)
@ -33,12 +32,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
}, },
} }
f, _ := os.Create("output.png")
res.Header().Set("Content-Type", "image/png") defer f.Close()
graph.Render(chart.PNG, res) graph.Render(chart.PNG, f)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,34 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func main() {
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 50,
Left: 25,
Right: 25,
Bottom: 10,
},
FillColor: drawing.ColorFromHex("efefef"),
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(),
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(100).WithMax(512)}.Values(),
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we set a custom range for the y-axis, overriding the automatic range generation. In this example we set a custom range for the y-axis, overriding the automatic range generation.
Note: the chart will still generate the ticks automatically based on the custom range, so the intervals may be a bit weird. Note: the chart will still generate the ticks automatically based on the custom range, so the intervals may be a bit weird.
@ -14,9 +16,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{ graph := chart.Chart{
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{ Range: &chart.ContinuousRange{
Min: 0.0, Min: 0.0,
Max: 10.0, Max: 10.0,
@ -29,12 +28,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
}, },
} }
f, _ := os.Create("output.png")
res.Header().Set("Content-Type", "image/png") defer f.Close()
graph.Render(chart.PNG, res) graph.Render(chart.PNG, f)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,13 +1,15 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"github.com/wcharczuk/go-chart/drawing" "os"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we set some custom colors for the series and the chart background and canvas. In this example we set some custom colors for the series and the chart background and canvas.
*/ */
@ -21,7 +23,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Style: chart.Style{ Style: chart.Style{
Show: true, //note; if we set ANY other properties, we must set this to true.
StrokeColor: drawing.ColorRed, // will supercede defaults StrokeColor: drawing.ColorRed, // will supercede defaults
FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults
}, },
@ -31,11 +32,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">\n<style type="text/css"><![CDATA[svg .background { fill: white; }svg .canvas { fill: white; }svg path.blue { fill: blue; stroke: lightblue; }svg path.green { fill: green; stroke: lightgreen; }svg path.gray { fill: gray; stroke: lightgray; }svg text.blue { fill: white; }svg text.green { fill: white; }svg text.gray { fill: white; }]]></style><path d="M 0 0
L 512 0
L 512 512
L 0 512
L 0 0" class="background"/><path d="M 5 5
L 507 5
L 507 507
L 5 507
L 5 5" class="canvas"/><path d="M 256 256
L 507 256
A 251 251 128.56 0 1 100 452
L 256 256
Z" class="blue"/><path d="M 256 256
L 100 452
A 251 251 128.56 0 1 201 12
L 256 256
Z" class="green"/><path d="M 256 256
L 201 12
A 251 251 102.85 0 1 506 256
L 256 256
Z" class="gray"/><text x="313" y="413" class="blue">Blue</text><text x="73" y="226" class="green">Green</text><text x="344" y="133" class="gray">Gray</text></svg>

After

Width:  |  Height:  |  Size: 987 B

View file

@ -0,0 +1,88 @@
package main
import (
"fmt"
"log"
"net/http"
"git.smarteching.com/zeni/go-chart/v2"
)
const style = "svg .background { fill: white; }" +
"svg .canvas { fill: white; }" +
"svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" +
"svg .green.fill.stroke { fill: green; stroke: lightgreen; }" +
"svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" +
"svg .blue.text { fill: white; }" +
"svg .green.text { fill: white; }" +
"svg .gray.text { fill: white; }"
func svgWithCustomInlineCSS(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", chart.ContentTypeSVG)
// Render the CSS with custom css
err := pieChart().Render(chart.SVGWithCSS(style, ""), res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
}
func svgWithCustomInlineCSSNonce(res http.ResponseWriter, _ *http.Request) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
// This should be randomly generated on every request!
const nonce = "RAND0MBASE64"
res.Header().Set("Content-Security-Policy", fmt.Sprintf("style-src 'nonce-%s'", nonce))
res.Header().Set("Content-Type", chart.ContentTypeSVG)
// Render the CSS with custom css and a nonce.
// Try changing the nonce to a different string - your browser should block the CSS.
err := pieChart().Render(chart.SVGWithCSS(style, nonce), res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
}
func svgWithCustomExternalCSS(res http.ResponseWriter, _ *http.Request) {
// Add external CSS
res.Write([]byte(
`<?xml version="1.0" standalone="no"?>` +
`<?xml-stylesheet href="/main.css" type="text/css"?>` +
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`))
res.Header().Set("Content-Type", chart.ContentTypeSVG)
err := pieChart().Render(chart.SVG, res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
}
func pieChart() chart.PieChart {
return chart.PieChart{
// Note that setting ClassName will cause all other inline styles to be dropped!
Background: chart.Style{ClassName: "background"},
Canvas: chart.Style{
ClassName: "canvas",
},
Width: 512,
Height: 512,
Values: []chart.Value{
{Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}},
{Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}},
{Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}},
},
}
}
func css(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/css")
res.Write([]byte(style))
}
func main() {
http.HandleFunc("/", svgWithCustomInlineCSS)
http.HandleFunc("/nonce", svgWithCustomInlineCSSNonce)
http.HandleFunc("/external", svgWithCustomExternalCSS)
http.HandleFunc("/main.css", css)
log.Fatal(http.ListenAndServe(":8080", nil))
}

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we set a custom set of ticks to use for the y-axis. It can be (almost) whatever you want, including some custom labels for ticks. In this example we set a custom set of ticks to use for the y-axis. It can be (almost) whatever you want, including some custom labels for ticks.
Custom ticks will supercede a custom range, which will supercede automatic generation based on series values. Custom ticks will supercede a custom range, which will supercede automatic generation based on series values.
@ -14,20 +16,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{ graph := chart.Chart{
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{ Range: &chart.ContinuousRange{
Min: 0.0, Min: 0.0,
Max: 4.0, Max: 4.0,
}, },
Ticks: []chart.Tick{ Ticks: []chart.Tick{
{0.0, "0.00"}, {Value: 0.0, Label: "0.00"},
{2.0, "2.00"}, {Value: 2.0, Label: "2.00"},
{4.0, "4.00"}, {Value: 4.0, Label: "4.00"},
{6.0, "6.00"}, {Value: 6.0, Label: "6.00"},
{8.0, "Eight"}, {Value: 8.0, Label: "Eight"},
{10.0, "Ten"}, {Value: 10.0, Label: "Ten"},
}, },
}, },
Series: []chart.Series{ Series: []chart.Series{
@ -37,12 +36,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
}, },
} }
f, _ := os.Create("output.png")
res.Header().Set("Content-Type", "image/png") defer f.Close()
graph.Render(chart.PNG, res) graph.Render(chart.PNG, f)
}
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
@ -19,18 +21,12 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
graph := chart.Chart{ graph := chart.Chart{
Height: 500, Height: 500,
Width: 500, Width: 500,
XAxis: chart.XAxis{ XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
/*Range: &chart.ContinuousRange{ /*Range: &chart.ContinuousRange{
Descending: true, Descending: true,
},*/ },*/
}, },
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: &chart.ContinuousRange{ Range: &chart.ContinuousRange{
Descending: true, Descending: true,
}, },
@ -38,7 +34,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Style: chart.Style{ Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64), FillColor: chart.GetDefaultColor(0).WithAlpha(64),
}, },
@ -48,11 +43,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,28 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
pie := chart.DonutChart{
Width: 512,
Height: 512,
Values: []chart.Value{
{Value: 5, Label: "Blue"},
{Value: 5, Label: "Green"},
{Value: 4, Label: "Gray"},
{Value: 4, Label: "Orange"},
{Value: 3, Label: "Deep Blue"},
{Value: 3, Label: "test"},
},
}
f, _ := os.Create("output.png")
defer f.Close()
pie.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">\n<path d="M 0 0
L 512 0
L 512 512
L 0 512
L 0 0" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 5 5
L 507 5
L 507 507
L 5 507
L 5 5" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 256 256
L 438 256
A 182 182 225.00 1 1 127 127
L 256 256
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(106,195,203,1.0)"/><path d="M 256 256
L 127 127
A 182 182 90.00 0 1 385 127
L 256 256
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(42,190,137,1.0)"/><path d="M 256 256
L 385 127
A 182 182 45.00 0 1 438 256
L 256 256
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(110,128,139,1.0)"/><path d="M 256 256
L 321 256
A 65 65 359.00 1 1 321 255
L 256 256
Z" style="stroke-width:4;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><text x="159" y="461" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Blue</text><text x="241" y="48" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Two</text><text x="440" y="181" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">One</text></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,222 @@
package main
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func main() {
chart.DefaultBackgroundColor = chart.ColorTransparent
chart.DefaultCanvasColor = chart.ColorTransparent
barWidth := 80
var (
colorWhite = drawing.Color{R: 241, G: 241, B: 241, A: 255}
colorMariner = drawing.Color{R: 60, G: 100, B: 148, A: 255}
colorLightSteelBlue = drawing.Color{R: 182, G: 195, B: 220, A: 255}
colorPoloBlue = drawing.Color{R: 126, G: 155, B: 200, A: 255}
colorSteelBlue = drawing.Color{R: 73, G: 120, B: 177, A: 255}
)
stackedBarChart := chart.StackedBarChart{
Title: "Quarterly Sales",
TitleStyle: chart.Shown(),
Background: chart.Style{
Padding: chart.Box{
Top: 75,
},
},
Width: 800,
Height: 600,
XAxis: chart.Shown(),
YAxis: chart.Shown(),
BarSpacing: 40,
IsHorizontal: true,
Bars: []chart.StackedBar{
{
Name: "Q1",
Width: barWidth,
Values: []chart.Value{
{
Label: "32K",
Value: 32,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorMariner,
FontColor: colorWhite,
},
},
{
Label: "46K",
Value: 46,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorLightSteelBlue,
FontColor: colorWhite,
},
},
{
Label: "48K",
Value: 48,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorPoloBlue,
FontColor: colorWhite,
},
},
{
Label: "42K",
Value: 42,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorSteelBlue,
FontColor: colorWhite,
},
},
},
},
{
Name: "Q2",
Width: barWidth,
Values: []chart.Value{
{
Label: "45K",
Value: 45,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorMariner,
FontColor: colorWhite,
},
},
{
Label: "60K",
Value: 60,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorLightSteelBlue,
FontColor: colorWhite,
},
},
{
Label: "62K",
Value: 62,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorPoloBlue,
FontColor: colorWhite,
},
},
{
Label: "53K",
Value: 53,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorSteelBlue,
FontColor: colorWhite,
},
},
},
},
{
Name: "Q3",
Width: barWidth,
Values: []chart.Value{
{
Label: "54K",
Value: 54,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorMariner,
FontColor: colorWhite,
},
},
{
Label: "58K",
Value: 58,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorLightSteelBlue,
FontColor: colorWhite,
},
},
{
Label: "55K",
Value: 55,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorPoloBlue,
FontColor: colorWhite,
},
},
{
Label: "47K",
Value: 47,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorSteelBlue,
FontColor: colorWhite,
},
},
},
},
{
Name: "Q4",
Width: barWidth,
Values: []chart.Value{
{
Label: "46K",
Value: 46,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorMariner,
FontColor: colorWhite,
},
},
{
Label: "70K",
Value: 70,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorLightSteelBlue,
FontColor: colorWhite,
},
},
{
Label: "74K",
Value: 74,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorPoloBlue,
FontColor: colorWhite,
},
},
{
Label: "60K",
Value: 60,
Style: chart.Style{
StrokeWidth: .01,
FillColor: colorSteelBlue,
FontColor: colorWhite,
},
},
},
},
},
}
pngFile, err := os.Create("output.png")
if err != nil {
panic(err)
}
if err := stackedBarChart.Render(chart.PNG, pngFile); err != nil {
panic(err)
}
if err := pngFile.Close(); err != nil {
panic(err)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/wcharczuk/go-chart" "git.smarteching.com/zeni/go-chart/v2"
) )
func main() { func main() {

View file

@ -1,12 +1,14 @@
package main package main
import ( //go:generate go run main.go
"net/http"
"github.com/wcharczuk/go-chart" import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
) )
func drawChart(res http.ResponseWriter, req *http.Request) { func main() {
/* /*
In this example we add a `Renderable` or a custom component to the `Elements` array. In this example we add a `Renderable` or a custom component to the `Elements` array.
@ -15,12 +17,6 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
*/ */
graph := chart.Chart{ graph := chart.Chart{
XAxis: chart.XAxis{
Style: chart.Style{Show: true},
},
YAxis: chart.YAxis{
Style: chart.Style{Show: true},
},
Background: chart.Style{ Background: chart.Style{
Padding: chart.Box{ Padding: chart.Box{
Top: 20, Top: 20,
@ -41,11 +37,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
chart.Legend(&graph), chart.Legend(&graph),
} }
res.Header().Set("Content-Type", "image/png") f, _ := os.Create("output.png")
graph.Render(chart.PNG, res) defer f.Close()
} graph.Render(chart.PNG, f)
func main() {
http.HandleFunc("/", drawChart)
http.ListenAndServe(":8080", nil)
} }

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