Compare commits

...

312 commits
v1.0 ... main

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
Will Charczuk
a0ea012903 fixing styling issues w/ the stack bar chart. 2017-06-11 11:38:10 -07:00
Jeff Willette
34de44488d changed the tick sanity ount variable (#42)
- The DefaultTickSanityCount was set in the default.go file and then in
in tick.go the setting was ignored and the value was again set with 1 << 10
Changed to use the setting instead
2017-05-22 20:11:29 -07:00
Will Charczuk
03708a90ef Helper API refactor (#40)
* api cleaup

* updates

* wtf

* updates

* snapshot.

* tweaks

* snapshot

* api tweaks.

* updates

* updates

* updates

* changes.

* updates

* updates

* sequence => seq

* dont need to use curl, just using wget

* fixing examples
2017-05-12 17:12:23 -07:00
Will Charczuk
43212f871f poly tests! 2017-04-26 00:27:11 -07:00
Will Charczuk
39e9977724 sane default handling 2017-04-25 23:35:07 -07:00
Will Charczuk
b244fd675c fixing tests 2017-04-25 20:03:30 -07:00
Will Charczuk
da432bc1db ??? 2017-04-25 20:01:48 -07:00
Will Charczuk
fc75f205ee tweaks 2017-04-24 23:06:50 -07:00
Will Charczuk
685186cc4d tweaks 2017-04-24 23:00:28 -07:00
Will Charczuk
64d2ef25c7 comment update 2017-04-21 13:34:54 -07:00
Will Charczuk
b41d05a2f4 adds last value for poly reg 2017-04-21 13:31:19 -07:00
Will Charczuk
0cbbd0887a parameterize by index. 2017-04-19 15:27:31 -07:00
Will Charczuk
cee1a66a50 cow -> col 2017-04-18 23:53:29 -07:00
Will Charczuk
a211e88530 Adds matrix sub package & adds polynomial regression series (#36)
* updates

* updates

* tests.

* test coverage

* fixing test

* stride not rows + cols

* lu decomp implementation.

* poly regression!

* poly regression works.

* typo.
2017-04-18 20:20:29 -07:00
Chris Redford
b3dc3fef3c Separation of DotColorProvider and ColorProvider Interfaces (#34)
* fully functioning Viridis

* pure functions
2017-04-17 16:21:02 -07:00
Didip Kerabat
e53554fb04 Benchmarking the performance of drawing line charts. (#30)
* Another example for benchmarking the performance of drawing line charts.

* Cleanup the code a bit.
2017-04-16 10:37:39 -07:00
Will Charczuk
3f046bbba1 adding jet as a generator 2017-04-15 09:59:37 -07:00
Will Charczuk
c599f1a2a2 removing color translator 2017-04-15 09:43:59 -07:00
Will Charczuk
03ac305951 just passing the full range. 2017-04-15 09:07:59 -07:00
Will Charczuk
079c471daf updated example 2017-04-14 17:46:53 -07:00
Will Charczuk
5a9b0753ec spelling is poor 2017-04-14 17:46:34 -07:00
Will Charczuk
fdb1b191b2 color maps 2017-04-14 17:43:52 -07:00
Will Charczuk
4f15ced303 adding helper 2017-03-26 16:29:24 -07:00
Will Charczuk
fec5be6506 adding helper 2017-03-26 16:27:15 -07:00
Will Charczuk
a44401b5ca adding helper 2017-03-26 16:21:52 -07:00
Will Charczuk
66e49377ce adding helper 2017-03-26 16:19:25 -07:00
Will Charczuk
66b99eb8e3 fixing svg circles 2017-03-05 23:52:13 -08:00
Will Charczuk
10de6fa9bf whitespace 2017-03-05 23:39:37 -08:00
Will Charczuk
8e91a3446f wiring up pprof. 2017-03-05 23:37:30 -08:00
Will Charczuk
ee78cf8f2f do actually need to check that the ranges are set 2017-03-05 23:28:35 -08:00
Will Charczuk
7a3d6a6915 also do this for the secondary axis 2017-03-05 23:21:39 -08:00
Will Charczuk
182d5a199b potential fix for issue with large deltas rounding the y axis 2017-03-05 23:20:58 -08:00
Will Charczuk
b713ff85cc Adds the ability to draw an XY scatter plot. (#27)
* works more or less

* updating comment

* removing debugging printf

* adding output

* tweaks

* missed a couple series validations

* testing auto coloring

* updated output.png

* color tests etc.

* sanity check tests.

* should not use unkeyed fields anyway.
2017-03-05 16:54:40 -08:00
Will Charczuk
17b28beae8 fixing range validation and tests. 2017-03-02 14:39:32 -08:00
Will Charczuk
b2de33058f tweaks to range validation. 2017-03-02 14:26:21 -08:00
Will Charczuk
114738a2de can fill values now 2017-02-28 17:55:48 -08:00
Will Charczuk
9a9af15fd4 Merge branch 'master' of github.com:wcharczuk/go-chart 2017-02-26 01:03:53 -08:00
Will Charczuk
5ff6f421d4 max 2017-02-26 01:03:47 -08:00
Will Charczuk
5bcde786bf these tests are not a joke 2017-02-22 17:48:32 -08:00
Will Charczuk
8b967158fd defaults are hard. 2017-02-22 17:47:08 -08:00
Will Charczuk
84e7baf2ef defaults 2017-02-22 17:44:44 -08:00
Will Charczuk
0cbdb3d88c transparent was also zero, needs to have values set. 2017-02-22 17:40:59 -08:00
Will Charczuk
f986c3c075 adding some color helpers 2017-02-22 17:34:23 -08:00
Will Charczuk
d88ba0ead3 removing exception dep, saving blank canvas on checkRanges err 2017-02-22 17:15:13 -08:00
Will Charczuk
a3605addc2 adding output. 2017-02-13 00:28:23 -08:00
Will Charczuk
6db18d540d snapshot. 2017-02-13 00:27:33 -08:00
Will Charczuk
1edf6193b2 slight tweak to legend (adds a new style) and example 2017-02-13 00:27:02 -08:00
Will Charczuk
00799114fa fixing windows est abbr 2017-02-12 14:09:00 -08:00
Will Charczuk
1584e50483 deferring creating market open times. 2017-02-12 09:10:21 -08:00
Will Charczuk
b9f2a35a5d fixing windows handling of eastern standard time 2017-02-12 09:02:14 -08:00
Will Charczuk
841f21921e example tweaks. 2017-02-06 14:26:37 -08:00
Will Charczuk
9b804502c0 tweaks 2017-02-06 14:25:09 -08:00
Will Charczuk
be0ad5560a chart test. 2017-02-03 11:27:58 -08:00
Will Charczuk
e735797037 adding validation. 2017-02-03 11:26:53 -08:00
Will Charczuk
3ea3c9ac10 example tweak. 2017-02-03 10:47:48 -08:00
Will Charczuk
7776c351a8 add custom value formatters for continuous series. 2017-01-13 16:03:45 -08:00
Will Charczuk
e4cd38f602 handle zero values 2017-01-13 14:37:08 -08:00
Will Charczuk
fc9b6a6d55 Merge branch 'master' of github.com:wcharczuk/go-chart 2017-01-13 14:07:47 -08:00
Will Charczuk
7ae42ee2e1 handle zero values with error. 2017-01-13 14:07:42 -08:00
Will Charczuk
f8e80fed56 Merge pull request #18 from relvacode/fix/bar-label-render
Fix multi-line bar chart label positioning
2017-01-10 13:56:09 -08:00
Will Charczuk
47a8a6b463 Merge pull request #17 from relvacode/master
Correct calculated height of bar chart
2017-01-10 13:55:53 -08:00
Will Charczuk
7163f89845 comment. 2017-01-10 13:54:57 -08:00
Will Charczuk
13cd06ec0d typo. 2017-01-10 13:52:34 -08:00
Will Charczuk
dfd8a00283 and for market hours as well. 2017-01-10 13:52:18 -08:00
Will Charczuk
98d3996b47 descending 2017-01-10 13:50:17 -08:00
Jason Kingsbury
e11622c154 render box labels directly to renderer 2017-01-10 14:25:06 +00:00
Will Charczuk
78cbfa62bc can just supply inverted ranges. 2017-01-09 17:57:45 -08:00
Jason Kingsbury
83016634b0 min instead of max height for bar xaxis height 2017-01-09 16:59:45 +00:00
Will Charczuk
21986dbce7 fixing output. 2016-12-15 18:07:07 -08:00
Will Charczuk
fc2973067c fixing y-axis label position 2016-10-27 10:40:27 -07:00
Will Charczuk
e8de11d590 removing .vscode 2016-10-23 12:10:37 -07:00
Will Charczuk
10c19374bc adding ignores. 2016-10-23 12:10:06 -07:00
Will Charczuk
4d7ea04118 comment. 2016-10-21 17:01:02 -07:00
Will Charczuk
27a5efdd2d Merge pull request #13 from wcharczuk/text-rotation
Adds `TextRotationDegrees`
2016-10-21 12:53:01 -07:00
Will Charczuk
2e8d196621 works, ish. 2016-10-21 12:50:40 -07:00
Will Charczuk
ac5cb1a00b missing files. 2016-10-21 12:44:44 -07:00
Will Charczuk
f800bc387b text rotation works, ish. 2016-10-21 12:44:37 -07:00
Will Charczuk
53280b9258 finalizing work on text rotation. 2016-10-20 15:21:52 -07:00
Will Charczuk
8595962d99 snapshot. 2016-09-11 09:13:57 -07:00
Will Charczuk
cd4bbc6503 tweaks 2016-09-05 17:31:29 -07:00
Will Charczuk
a1835a532d bounded rotate works, ish 2016-09-05 14:03:20 -07:00
Will Charczuk
8f56e5939b snapshot. 2016-09-05 13:26:12 -07:00
Will Charczuk
b78f2327aa text rotation is sucky. 2016-08-31 22:11:52 -07:00
Will Charczuk
78e6288748 image collector 2016-08-27 14:24:15 -07:00
Will Charczuk
ce5b36a6cb image collector. 2016-08-27 14:23:55 -07:00
Will Charczuk
6fbc6caa9c axes label example and thin legend. 2016-08-27 13:45:38 -07:00
Will Charczuk
dd89d27af5 axes label example 2016-08-27 12:55:08 -07:00
Will Charczuk
102f7a8aa3 fixing build. 2016-08-11 22:24:40 -07:00
Will Charczuk
9c53de511a tweaking example. 2016-08-11 20:43:38 -07:00
Will Charczuk
2e41dfd5db changing gridline behavior. 2016-08-11 20:42:25 -07:00
Will Charczuk
3735edb5be example tweaks. 2016-08-11 20:38:53 -07:00
Will Charczuk
7885ca4d69 updating output. 2016-08-11 15:56:04 -07:00
Will Charczuk
42af835f0e doing the same for yaxis. 2016-08-11 15:52:15 -07:00
Will Charczuk
7951d97d82 tweaking xaxis gridline handling. 2016-08-11 15:51:49 -07:00
Will Charczuk
141327f898 getting rid of redundant examples 2016-08-08 17:19:26 -07:00
Will Charczuk
50cecc47bd adding output for examples. 2016-08-08 11:03:12 -07:00
Will Charczuk
6f393c5b08 readme updates. 2016-08-07 11:05:57 -07:00
Will Charczuk
c19111629c report card 2016-08-07 10:51:06 -07:00
Will Charczuk
63a26acadd zero line? 2016-08-07 00:33:48 -07:00
Will Charczuk
a74c8710df fixing xaxis measure bug leading to too much left padding. 2016-08-06 23:58:44 -07:00
Will Charczuk
a846d2f4e8 medium support for all axis names. 2016-08-06 22:27:26 -07:00
Will Charczuk
f7c53080bd readme updates. 2016-08-06 22:00:22 -07:00
Will Charczuk
718678b421 can rotate text + add y axis names 2016-08-06 21:59:46 -07:00
Will Charczuk
3607d732d9 last annotation helper 2016-08-06 15:53:14 -07:00
Will Charczuk
3b02935861 tweaks. 2016-08-06 00:28:12 -07:00
Will Charczuk
e4e2c846e3 hit coverage min, but not quite done yet. 2016-08-06 00:26:17 -07:00
Will Charczuk
22e4431c82 tests catching bugs. 2016-08-05 21:31:41 -07:00
Will Charczuk
129cbad0fa tests. 2016-08-05 21:25:56 -07:00
Will Charczuk
54f3bd4d3a coverage goal is 70% by the end of the night. 2016-08-05 20:44:40 -07:00
Will Charczuk
95d0bcca4b tests 2016-08-05 20:08:24 -07:00
Will Charczuk
7bb3fbf810 bar charts! 2016-08-05 16:55:55 -07:00
Will Charczuk
818dd0113b Merge pull request #8 from wcharczuk/wcharczuk/market-hours
Consolidating date functions & tweaking market hours range
2016-08-01 09:03:08 -07:00
Will Charczuk
b1cd8bd2e3 market hours tweaks. 2016-08-01 00:50:32 -07:00
Will Charczuk
c3a066aecd tweaks to make ticks not be terrible 2016-07-31 16:54:09 -07:00
Will Charczuk
7b5bb6e952 changing how Draw.LineSeries handles the 0 line 2016-07-30 23:14:19 -07:00
Will Charczuk
6d2a61b790 fixing market hours 2016-07-30 21:34:41 -07:00
Will Charczuk
200116c193 fixing sanity check test on gen ticks. 2016-07-30 20:30:19 -07:00
Will Charczuk
7a9d9e56a7 fixing tests. 2016-07-30 20:20:59 -07:00
Will Charczuk
5e15568903 need to rewrite this test. 2016-07-30 12:59:59 -07:00
Will Charczuk
0fe9f53806 fixing things with generated ticks. 2016-07-30 12:57:18 -07:00
Will Charczuk
457fefbc6d fixing undertick 2016-07-30 10:01:16 -07:00
Will Charczuk
bf962d256a adding back default font color for annotations. 2016-07-30 09:52:18 -07:00
Will Charczuk
1af266dc7e Merge pull request #6 from wcharczuk/v2.0-devel
moving over to v2.0
2016-07-30 09:41:59 -07:00
Will Charczuk
60a9a3d6fc readme. 2016-07-30 09:39:50 -07:00
Will Charczuk
bf38f3e718 tests are pretty helpful 2016-07-30 09:38:43 -07:00
Will Charczuk
9c96af2a22 fixing fit issues on the xaxis labels for stacked bar. 2016-07-30 09:35:44 -07:00
Will Charczuk
9b4307e186 axis tick position 2016-07-30 09:12:03 -07:00
Will Charczuk
c2d680968d tests 2016-07-29 23:39:58 -07:00
Will Charczuk
12d115cca5 examples. 2016-07-29 23:31:49 -07:00
Will Charczuk
8b6afae778 fixed spacing issues. 2016-07-29 23:13:33 -07:00
Will Charczuk
e5cc7f9e9c looking spiffy. 2016-07-29 19:40:41 -07:00
Will Charczuk
1269397821 sbc looking alright, need to debug bar gaps at the bottom 2016-07-29 19:38:37 -07:00
Will Charczuk
e44cdd5600 where did my ticks go. 2016-07-29 19:35:18 -07:00
Will Charczuk
a008ebe30e tweaks. 2016-07-29 18:40:43 -07:00
Will Charczuk
cbc0002d2a big api overhauls. 2016-07-29 18:24:25 -07:00
Will Charczuk
d84d6790c0 text options! 2016-07-29 16:36:29 -07:00
Will Charczuk
b3386853bb readme. 2016-07-28 19:19:26 -07:00
Will Charczuk
88b6b2274d adds SliceStyle for pie chart. 2016-07-28 19:17:35 -07:00
Will Charczuk
df971b61d1 pie chart image && build fix. 2016-07-28 19:00:48 -07:00
Will Charczuk
fded346ae7 fixing example. 2016-07-28 18:58:45 -07:00
Will Charczuk
490d7dae38 test updates. 2016-07-28 18:56:45 -07:00
Will Charczuk
a1fb284797 basics of stacked bar. 2016-07-28 18:51:55 -07:00
Will Charczuk
020ec8f4a4 coverage at 65.4% 2016-07-28 16:40:29 -07:00
Will Charczuk
bff8e074fd testing histogram series. 2016-07-28 16:36:30 -07:00
Will Charczuk
d088213b1e fixing travis bust. 2016-07-28 14:35:17 -07:00
Will Charczuk
4d41de533f travis. 2016-07-28 14:32:23 -07:00
Will Charczuk
84df29b1c6 need to flesh out this test more. 2016-07-28 14:30:00 -07:00
Will Charczuk
3d9cf0da0c vector renderer works 2016-07-28 13:22:18 -07:00
Will Charczuk
b600cb1994 examples 2016-07-28 02:43:28 -07:00
Will Charczuk
c17c9a4bb4 pie charts! 2016-07-28 02:34:44 -07:00
Will Charczuk
ec4d92fc5e FENCEPOSTS. 2016-07-27 12:54:40 -07:00
Will Charczuk
fc0f274f51 fixing handling of offset and window 2016-07-27 12:44:02 -07:00
Will Charczuk
6533e951e7 tests. 2016-07-27 12:34:15 -07:00
Will Charczuk
4f381fa4dc merge 2016-07-27 08:23:53 -07:00
Will Charczuk
ef1c38a641 fixing day of market results. 2016-07-27 08:21:05 -07:00
Will Charczuk
40ff878b79 things. 2016-07-27 00:40:22 -07:00
Will Charczuk
0c049db317 fixing calculate on day. 2016-07-27 00:34:10 -07:00
Will Charczuk
865ba96eb5 switching the formatter 2016-07-27 00:20:43 -07:00
Will Charczuk
05caeb41ee fixing test. 2016-07-26 23:55:31 -07:00
Will Charczuk
3bf31e45d3 market hours range not quite working yet. 2016-07-24 22:54:03 -07:00
Will Charczuk
50a798f67f style handling on gridlines. 2016-07-24 20:27:19 -07:00
Will Charczuk
2f7cd11039 Merge branch 'v2.0-devel' of github.com:wcharczuk/go-chart into v2.0-devel 2016-07-24 13:48:15 -07:00
Will Charczuk
b22d565d44 ?? 2016-07-24 13:48:10 -07:00
Will Charczuk
67e291ef9b dep resolution 2016-07-24 11:45:30 -07:00
Will Charczuk
8fbb8d9775 tests. 2016-07-24 09:04:07 -07:00
Will Charczuk
e3ae7fd78f something broke with before: 2016-07-23 16:58:00 -07:00
Will Charczuk
bf30704796 jfc 2016-07-23 16:56:39 -07:00
Will Charczuk
1bc681e52a sadface re: build. 2016-07-23 16:55:16 -07:00
Will Charczuk
4424c34063 explicit deps. 2016-07-23 16:53:46 -07:00
Will Charczuk
02d9ccaed9 things. 2016-07-23 16:51:17 -07:00
Will Charczuk
b766bc2127 travis got weird. 2016-07-23 16:50:07 -07:00
Will Charczuk
a6b6097c20 ticks refactor. 2016-07-23 15:35:49 -07:00
Will Charczuk
78645130e4 refinements. 2016-07-23 13:01:38 -07:00
Will Charczuk
d41c9313aa tests pass post refactor. 2016-07-23 12:58:37 -07:00
Will Charczuk
fd2bfe14f0 refactoring things a bit. 2016-07-23 11:50:30 -07:00
Will Charczuk
859c573d3d refactors 2016-07-22 22:43:27 -07:00
Will Charczuk
dea8e71d6f making this pass range interface. 2016-07-21 22:22:22 -07:00
Will Charczuk
c4066176cf date, nyse market hours range. 2016-07-21 22:09:09 -07:00
Will Charczuk
c2f7c99c3f fixing build bust. 2016-07-21 14:14:28 -07:00
Will Charczuk
b0934ee2e3 introduces the Range interface (instead of a concrete type). 2016-07-21 14:11:27 -07:00
Will Charczuk
8af50213c3 fixing macd and ema performance issues. 2016-07-18 18:29:08 -07:00
Will Charczuk
befd626bbe fixing macd inversion. 2016-07-18 17:41:59 -07:00
Will Charczuk
f572279469 tweak for line overlap. 2016-07-18 17:15:24 -07:00
Will Charczuk
f3a6eb0960 fixing sigma calc on empty period. 2016-07-18 17:02:14 -07:00
Will Charczuk
6afff9b23b fixing build bus. 2016-07-18 17:00:58 -07:00
Will Charczuk
1634ced521 shouldnt be able to override. 2016-07-18 16:59:37 -07:00
Will Charczuk
034bef0118 changing ema defaults. 2016-07-18 16:59:13 -07:00
Will Charczuk
2377467059 fixing test. 2016-07-18 15:49:34 -07:00
Will Charczuk
3c3e045937 naming is important. 2016-07-18 15:47:33 -07:00
Will Charczuk
8ae941be24 bad interface format 2016-07-18 15:44:22 -07:00
Will Charczuk
4d6c22a45c switching outer macd to use inner signal series. 2016-07-18 15:13:39 -07:00
Will Charczuk
bf690767f4 missed one. 2016-07-18 15:08:50 -07:00
Will Charczuk
6d6778d729 s/window/period/g 2016-07-18 15:06:42 -07:00
Will Charczuk
f1376c6566 further c/p issues. 2016-07-18 14:54:03 -07:00
Will Charczuk
15c94b1e22 copy / paste issues. 2016-07-18 14:53:45 -07:00
Will Charczuk
e2cf11729b adding helper interfaces 2016-07-18 14:53:29 -07:00
Will Charczuk
2603c67e10 adding lastvalueprovider interface and relevant methods. 2016-07-18 12:57:10 -07:00
Will Charczuk
c14ab6a4b2 tweaks to fix annotation series drawing. 2016-07-18 11:53:29 -07:00
Will Charczuk
01992b9afb removing hard default on draw annotation. 2016-07-18 11:47:23 -07:00
Will Charczuk
8edf6a5e8a adding style.String() 2016-07-18 11:43:41 -07:00
Will Charczuk
f661cfc8c1 removing debug statements. 2016-07-17 22:37:54 -07:00
Will Charczuk
98c46b62d2 macd works with test. 2016-07-17 18:54:50 -07:00
Will Charczuk
4f8680a2b1 small fixes for legend + background padding. 2016-07-17 16:58:52 -07:00
Will Charczuk
eb61fbfd0f test fixes 2016-07-17 14:32:27 -07:00
Will Charczuk
cea31c6c23 response times example needs some ergonomic love. 2016-07-17 14:25:42 -07:00
Will Charczuk
7ae7cc13a8 stock chart analysis. 2016-07-17 13:29:01 -07:00
Will Charczuk
50233991ca fixing an issue with forcing the secondary y axis on when there were no series mapped to it. 2016-07-17 13:06:07 -07:00
Will Charczuk
ae31a618df custom padding example and slight fixes. 2016-07-17 12:52:26 -07:00
Will Charczuk
0e3d2e9c1f changing how we handle default padding. 2016-07-17 11:24:54 -07:00
Will Charczuk
630885bd27 fixing example 2016-07-17 11:12:44 -07:00
Will Charczuk
32dd907bf8 more iterating on macd, need to get a better handle on how to test. 2016-07-17 11:10:04 -07:00
Will Charczuk
7858457772 histogram still needs tweaking. 2016-07-17 01:42:31 -07:00
Will Charczuk
d455b775da small bugfix for edge case. 2016-07-16 23:45:28 -07:00
Will Charczuk
3432bd9443 adding timeseries example. 2016-07-16 23:03:01 -07:00
Will Charczuk
ed452c5a7a bad comment. 2016-07-16 22:56:30 -07:00
Will Charczuk
6d48c49c07 tests for some things that came up as regressions when building out examples. 2016-07-16 22:56:12 -07:00
Will Charczuk
230dd16308 Update README.md 2016-07-16 21:21:50 -07:00
Will Charczuk
f612f5a344 merge 2016-07-16 20:54:50 -07:00
Will Charczuk
ac26f764eb examples, some fixes 2016-07-16 20:53:46 -07:00
Will Charczuk
4f4b0c3815 Update README.md 2016-07-16 15:03:00 -07:00
Will Charczuk
2adc3c7fdd exp. last value and test. 2016-07-16 13:25:21 -07:00
Will Charczuk
8bd5cdfe17 exp moving average, renaming moving average to simple moving average. 2016-07-16 13:10:44 -07:00
Will Charczuk
9ad15b3288 fixing dash arrays and style handling in vector renderer. 2016-07-15 18:19:29 -07:00
Will Charczuk
98cda14767 Merge branch 'master' of github.com:wcharczuk/go-chart 2016-07-15 17:17:46 -07:00
Will Charczuk
9a19bf094b adding a helper method, DrawBoundedSeries to help w/ bounded series drawing. 2016-07-15 17:15:06 -07:00
Will Charczuk
9960ea5e0c Update README.md 2016-07-15 17:01:51 -07:00
Will Charczuk
3c9f7032a3 readme updates. 2016-07-15 17:00:50 -07:00
Will Charczuk
67eccff04d adding image. 2016-07-15 16:58:27 -07:00
Will Charczuk
0defd8dd2a more gofmt -s changes. 2016-07-15 13:45:33 -07:00
Will Charczuk
296ed0fd3b gofmt -s 2016-07-15 13:43:53 -07:00
Will Charczuk
00ff859126 DID NOT KNOW THAT 2016-07-15 13:42:04 -07:00
Will Charczuk
1c6df58320 some report card feedback. 2016-07-15 13:40:24 -07:00
Will Charczuk
f8573f1123 adding GetLastBoundedValue and tests 2016-07-15 13:27:45 -07:00
Will Charczuk
d888440415 tests. 2016-07-15 09:17:51 -07:00
Will Charczuk
bc2b51077b bollinger bounds 2016-07-15 09:02:50 -07:00
Will Charczuk
21b4dfddc9 these tests are not a joke. 2016-07-14 21:16:49 -07:00
Will Charczuk
15f5355013 padding tweak. 2016-07-14 21:14:46 -07:00
Will Charczuk
9532174fd1 Update README.md 2016-07-14 20:52:05 -07:00
215 changed files with 15484 additions and 2613 deletions

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

20
.gitignore vendored Normal file
View file

@ -0,0 +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
.DS_Store
coverage.html
.idea

View file

@ -1,13 +0,0 @@
language: go
go:
- 1.6.2
sudo: false
before_script:
- go build -i ./...
script:
- go test
- go test ./drawing/

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

10
Makefile Normal file
View file

@ -0,0 +1,10 @@
all: new-install test
new-install:
@go get -v -u ./...
generate:
@go generate ./...
test:
@go test ./...

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"

218
README.md
View file

@ -1,187 +1,91 @@
go-chart go-chart
======== ========
[![Build Status](https://travis-ci.org/wcharczuk/go-chart.svg?branch=master)](https://travis-ci.org/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 API is still in a bit of flux, so it is adviseable to wait until I tag a v1.0 release before using -
in a production capacity.
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.
# Usage # Output Examples
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/goog_ltm.png) Spark Lines:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/tvix_ltm.png)
The chart code to produce the above is as follows: Single axis:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/goog_ltm.png)
Two axis:
![](https://git.smarteching.com/zeni/go-chart/raw/branch/main/_images/two_axis.png)
# Other Chart Types
Pie Chart:
![](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`.
Stacked Bar:
![](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`.
# Code Examples
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
Everything starts with the `chart.Chart` object. The bare minimum to draw a chart would be the following:
```golang
import (
...
"bytes"
...
"git.smarteching.com/zeni/go-chart/v2" //exposes "chart"
)
```go
// note this assumes that xvalues and yvalues
// have been pulled from a pricing service.
graph := chart.Chart{ graph := chart.Chart{
Width: 1024,
Height: 400,
YAxis: chart.YAxis {
Style: chart.Style{
Show: true,
},
},
XAxis: chart.XAxis {
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{ Series: []chart.Series{
chart.TimeSeries{ chart.ContinuousSeries{
XValues: xvalues, XValues: []float64{1.0, 2.0, 3.0, 4.0},
YValues: yvalues, YValues: []float64{1.0, 2.0, 3.0, 4.0},
Style: chart.Style {
FillColor: chart.DefaultSeriesStrokeColors[0].WithAlpha(64),
},
},
chart.AnnotationSeries{
Name: "Last Value",
Style: chart.Style{
Show: true,
StrokeColor: chart.DefaultSeriesStrokeColors[0],
},
Annotations: []chart.Annotation{
chart.Annotation{
X: chart.TimeToFloat64(xvalues[len(xvalues)-1]),
Y: yvalues[len(yvalues)-1],
Label: chart.FloatValueFormatter(yvalues[len(yvalues)-1]),
},
},
}, },
}, },
} }
graph.Render(chart.PNG, buffer) //thats it!
buffer := bytes.NewBuffer([]byte{})
err := graph.Render(chart.PNG, buffer)
``` ```
The key areas to note are that we have to explicitly turn on two features, the axes and add the last value label annotation series. When calling `.Render(..)` we add a parameter, `chart.PNG` that tells the renderer to use a raster renderer. Another option is to use `chart.SVG` which will use the vector renderer and create an svg representation of the chart. Explanation of the above: A `chart` can have many `Series`, a `Series` is a collection of things that need to be drawn according to the X range and the Y range(s).
# Alternate Usage Here, we have a single series with x range values as float64s, rendered to a PNG. Note; we can pass any type of `io.Writer` into `Render(...)`, meaning that we can render the chart to a file or a resonse or anything else that implements `io.Writer`.
You can alternately leave a bunch of features turned off and constrain the proportions to something like a spark line: # API Overview
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/tvix_ltm.png) 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.
The code to produce the above would be: The best way to see the api in action is to look at the examples in the `./_examples/` directory.
```go
// note this assumes that xvalues and yvalues
// have been pulled from a pricing service.
graph := chart.Chart{
Width: 1024,
Height: 100,
Series: []chart.Series{
chart.TimeSeries{
XValues: xvalues,
YValues: yvalues,
},
},
}
graph.Render(chart.PNG, buffer)
```
# 2 Y-Axis Charts
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/two_axis.png)
It is also possible to draw series against 2 separate y-axis with their own ranges (usually good for comparison charts).
In order to map the series to an alternate axis make sure to set the `YAxis` property of the series to `YAxisSecondary`.
```go
graph := chart.Chart{
Title: stock.Name,
TitleStyle: chart.Style{
Show: false,
},
Width: width,
Height: height,
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
},
Series: []chart.Series{
chart.TimeSeries{
Name: "vea",
XValues: vx,
YValues: vy,
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
},
},
chart.TimeSeries{
Name: "spy",
XValues: cx,
YValues: cy,
YAxis: chart.YAxisSecondary, // key (!)
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(1),
FillColor: chart.GetDefaultSeriesStrokeColor(1).WithAlpha(64),
},
},
chart.AnnotationSeries{
Name: fmt.Sprintf("%s - Last Value", "vea"),
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
},
Annotations: []chart.Annotation{
chart.Annotation{
X: float64(vx[len(vx)-1].Unix()),
Y: vy[len(vy)-1],
Label: fmt.Sprintf("%s - %s", "vea", chart.FloatValueFormatter(vy[len(vy)-1])),
},
},
},
chart.AnnotationSeries{
Name: fmt.Sprintf("%s - Last Value", "goog"),
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(1),
},
YAxis: chart.YAxisSecondary, // key (!)
Annotations: []chart.Annotation{
chart.Annotation{
X: float64(cx[len(cx)-1].Unix()),
Y: cy[len(cy)-1],
Label: fmt.Sprintf("%s - %s", "goog", chart.FloatValueFormatter(cy[len(cy)-1])),
},
},
},
},
}
graph.Render(chart.PNG, buffer)
```
# Moving Averages
You can now also graph a moving average of a series using a special `MovingAverageSeries` that takes an `InnerSeries` as a required argument.
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/ma_goog_ltm.png)
There is a helper method, `GetLastValue` on the `MovingAverageSeries` to aid in creating a last value annotation for the series.
# Design Philosophy # Design Philosophy
@ -191,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

BIN
_images/bar_chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
_images/pie_chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
_images/spy_ltm_bbs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
_images/stacked_bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -1,19 +1,21 @@
package chart package chart
import "math" import (
"fmt"
"math"
)
// Annotation is a label on the chart. // Interface Assertions.
type Annotation struct { var (
X, Y float64 _ Series = (*AnnotationSeries)(nil)
Label string )
}
// AnnotationSeries is a series of labels on the chart. // AnnotationSeries is a series of labels on the chart.
type AnnotationSeries struct { type AnnotationSeries struct {
Name string Name string
Style Style Style Style
YAxis YAxisType YAxis YAxisType
Annotations []Annotation Annotations []Value2
} }
// GetName returns the name of the time series. // GetName returns the name of the time series.
@ -31,6 +33,18 @@ func (as AnnotationSeries) GetYAxis() YAxisType {
return as.YAxis return as.YAxis
} }
func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style {
return Style{
FontColor: DefaultTextColor,
Font: defaults.Font,
FillColor: DefaultAnnotationFillColor,
FontSize: DefaultAnnotationFontSize,
StrokeColor: defaults.StrokeColor,
StrokeWidth: defaults.StrokeWidth,
Padding: DefaultAnnotationPadding,
}
}
// Measure returns a bounds box of the series. // Measure returns a bounds box of the series.
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box { func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
box := Box{ box := Box{
@ -39,19 +53,13 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
Right: 0, Right: 0,
Bottom: 0, Bottom: 0,
} }
if as.Style.Show { if !as.Style.Hidden {
style := as.Style.WithDefaultsFrom(Style{ seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
Font: defaults.Font,
FillColor: DefaultAnnotationFillColor,
FontSize: DefaultAnnotationFontSize,
StrokeColor: defaults.StrokeColor,
StrokeWidth: defaults.StrokeWidth,
Padding: DefaultAnnotationPadding,
})
for _, a := range as.Annotations { for _, a := range as.Annotations {
lx := canvasBox.Left + xrange.Translate(a.X) style := a.Style.InheritFrom(seriesStyle)
ly := canvasBox.Bottom - yrange.Translate(a.Y) lx := canvasBox.Left + xrange.Translate(a.XValue)
ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label) ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = MinInt(box.Top, ab.Top) box.Top = MinInt(box.Top, ab.Top)
box.Left = MinInt(box.Left, ab.Left) box.Left = MinInt(box.Left, ab.Left)
box.Right = MaxInt(box.Right, ab.Right) box.Right = MaxInt(box.Right, ab.Right)
@ -63,20 +71,21 @@ 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.Show { if !as.Style.Hidden {
style := as.Style.WithDefaultsFrom(Style{ seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
Font: defaults.Font,
FontColor: DefaultTextColor,
FillColor: DefaultAnnotationFillColor,
FontSize: DefaultAnnotationFontSize,
StrokeColor: defaults.StrokeColor,
StrokeWidth: defaults.StrokeWidth,
Padding: DefaultAnnotationPadding,
})
for _, a := range as.Annotations { for _, a := range as.Annotations {
lx := canvasBox.Left + xrange.Translate(a.X) style := a.Style.InheritFrom(seriesStyle)
ly := canvasBox.Bottom - yrange.Translate(a.Y) lx := canvasBox.Left + xrange.Translate(a.XValue)
DrawAnnotation(r, canvasBox, style, lx, ly, a.Label) ly := canvasBox.Bottom - yrange.Translate(a.YValue)
Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
} }
} }
} }
// Validate validates the series.
func (as AnnotationSeries) Validate() error {
if len(as.Annotations) == 0 {
return fmt.Errorf("annotation series requires annotations to be set and not empty")
}
return nil
}

View file

@ -4,37 +4,34 @@ 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{ Annotations: []Value2{
Show: true, {XValue: 1.0, YValue: 1.0, Label: "1.0"},
}, {XValue: 2.0, YValue: 2.0, Label: "2.0"},
Annotations: []Annotation{ {XValue: 3.0, YValue: 3.0, Label: "3.0"},
Annotation{X: 1.0, Y: 1.0, Label: "1.0"}, {XValue: 4.0, YValue: 4.0, Label: "4.0"},
Annotation{X: 2.0, Y: 2.0, Label: "2.0"},
Annotation{X: 3.0, Y: 3.0, Label: "3.0"},
Annotation{X: 4.0, Y: 4.0, Label: "4.0"},
}, },
} }
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 := Range{ xrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
Max: 4.0, Max: 4.0,
Domain: 100, Domain: 100,
} }
yrange := Range{ yrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
Max: 4.0, Max: 4.0,
Domain: 100, Domain: 100,
@ -52,42 +49,41 @@ 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(-3.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(147.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,
}, },
Annotations: []Annotation{ Annotations: []Value2{
Annotation{X: 1.0, Y: 1.0, Label: "1.0"}, {XValue: 1.0, YValue: 1.0, Label: "1.0"},
Annotation{X: 2.0, Y: 2.0, Label: "2.0"}, {XValue: 2.0, YValue: 2.0, Label: "2.0"},
Annotation{X: 3.0, Y: 3.0, Label: "3.0"}, {XValue: 3.0, YValue: 3.0, Label: "3.0"},
Annotation{X: 4.0, Y: 4.0, Label: "4.0"}, {XValue: 4.0, YValue: 4.0, Label: "4.0"},
}, },
} }
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 := Range{ xrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
Max: 4.0, Max: 4.0,
Domain: 100, Domain: 100,
} }
yrange := Range{ yrange := &ContinuousRange{
Min: 1.0, Min: 1.0,
Max: 4.0, Max: 4.0,
Domain: 100, Domain: 100,
@ -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)
} }

24
array.go Normal file
View file

@ -0,0 +1,24 @@
package chart
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 {
return Array(values)
}
// Array is a wrapper for an array of floats that implements `ValuesProvider`.
type Array []float64
// Len returns the value provider length.
func (a Array) Len() int {
return len(a)
}
// GetValue returns the value at a given index.
func (a Array) GetValue(index int) float64 {
return a[index]
}

29
axis.go
View file

@ -1,5 +1,17 @@
package chart package chart
// TickPosition is an enumeration of possible tick drawing positions.
type TickPosition int
const (
// TickPositionUnset means to use the default tick position.
TickPositionUnset TickPosition = 0
// TickPositionBetweenTicks draws the labels for a tick between the previous and current tick.
TickPositionBetweenTicks TickPosition = 1
// TickPositionUnderTick draws the tick below the tick.
TickPositionUnderTick TickPosition = 2
)
// YAxisType is a type of y-axis; it can either be primary or secondary. // YAxisType is a type of y-axis; it can either be primary or secondary.
type YAxisType int type YAxisType int
@ -13,8 +25,21 @@ const (
// Axis is a chart feature detailing what values happen where. // Axis is a chart feature detailing what values happen where.
type Axis interface { type Axis interface {
GetName() string GetName() string
SetName(name string)
GetStyle() Style GetStyle() Style
GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick SetStyle(style Style)
GetTicks() []Tick
GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
// GenerateGridLines returns the gridlines for the axis.
GetGridLines(ticks []Tick) []GridLine GetGridLines(ticks []Tick) []GridLine
Render(c *Chart, r Renderer, canvasBox Box, ra Range, ticks []Tick)
// Measure should return an absolute box for the axis.
// This is used when auto-fitting the canvas to the background.
Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box
// Render renders the axis.
Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick)
} }

491
bar_chart.go Normal file
View file

@ -0,0 +1,491 @@
package chart
import (
"errors"
"fmt"
"io"
"math"
"github.com/golang/freetype/truetype"
)
// BarChart is a chart that draws bars on a range.
type BarChart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
BarWidth int
Background Style
Canvas Style
XAxis Style
YAxis YAxis
BarSpacing int
UseBaseValue bool
BaseValue float64
Font *truetype.Font
defaultFont *truetype.Font
Bars []Value
Elements []Renderable
}
// GetDPI returns the dpi for the chart.
func (bc BarChart) GetDPI() float64 {
if bc.DPI == 0 {
return DefaultDPI
}
return bc.DPI
}
// GetFont returns the text font.
func (bc BarChart) GetFont() *truetype.Font {
if bc.Font == nil {
return bc.defaultFont
}
return bc.Font
}
// GetWidth returns the chart width or the default value.
func (bc BarChart) GetWidth() int {
if bc.Width == 0 {
return DefaultChartWidth
}
return bc.Width
}
// GetHeight returns the chart height or the default value.
func (bc BarChart) GetHeight() int {
if bc.Height == 0 {
return DefaultChartHeight
}
return bc.Height
}
// GetBarSpacing returns the spacing between bars.
func (bc BarChart) GetBarSpacing() int {
if bc.BarSpacing == 0 {
return DefaultBarSpacing
}
return bc.BarSpacing
}
// GetBarWidth returns the default bar width.
func (bc BarChart) GetBarWidth() int {
if bc.BarWidth == 0 {
return DefaultBarWidth
}
return bc.BarWidth
}
// Render renders the chart with the given renderer to the given io.Writer.
func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
if len(bc.Bars) == 0 {
return errors.New("please provide at least one bar")
}
r, err := rp(bc.GetWidth(), bc.GetHeight())
if err != nil {
return err
}
if bc.Font == nil {
defaultFont, err := GetDefaultFont()
if err != nil {
return err
}
bc.defaultFont = defaultFont
}
r.SetDPI(bc.GetDPI())
bc.drawBackground(r)
var canvasBox Box
var yt []Tick
var yr Range
var yf ValueFormatter
canvasBox = bc.getDefaultCanvasBox()
yr = bc.getRanges()
if yr.GetMax()-yr.GetMin() == 0 {
return fmt.Errorf("invalid data range; cannot be zero")
}
yr = bc.setRangeDomains(canvasBox, yr)
yf = bc.getValueFormatters()
if bc.hasAxes() {
yt = bc.getAxesTicks(r, yr, yf)
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
yr = bc.setRangeDomains(canvasBox, yr)
}
bc.drawCanvas(r, canvasBox)
bc.drawBars(r, canvasBox, yr)
bc.drawXAxis(r, canvasBox)
bc.drawYAxis(r, canvasBox, yr, yt)
bc.drawTitle(r)
for _, a := range bc.Elements {
a(r, canvasBox, bc.styleDefaultsElements())
}
return r.Save(w)
}
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, bc.getCanvasStyle())
}
func (bc BarChart) getRanges() Range {
var yrange Range
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
yrange = bc.YAxis.Range
} else {
yrange = &ContinuousRange{}
}
if !yrange.IsZero() {
return yrange
}
if len(bc.YAxis.Ticks) > 0 {
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
for _, t := range bc.YAxis.Ticks {
tickMin = math.Min(tickMin, t.Value)
tickMax = math.Max(tickMax, t.Value)
}
yrange.SetMin(tickMin)
yrange.SetMax(tickMax)
return yrange
}
min, max := math.MaxFloat64, -math.MaxFloat64
for _, b := range bc.Bars {
min = math.Min(b.Value, min)
max = math.Max(b.Value, max)
}
yrange.SetMin(min)
yrange.SetMax(max)
return yrange
}
func (bc BarChart) drawBackground(r Renderer) {
Draw.Box(r, Box{
Right: bc.GetWidth(),
Bottom: bc.GetHeight(),
}, bc.getBackgroundStyle())
}
func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
xoffset := canvasBox.Left
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
bs2 := spacing >> 1
var barBox Box
var bxl, bxr, by int
for index, bar := range bc.Bars {
bxl = xoffset + bs2
bxr = bxl + width
by = canvasBox.Bottom - yr.Translate(bar.Value)
if bc.UseBaseValue {
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
}
} else {
barBox = Box{
Top: by,
Left: bxl,
Right: bxr,
Bottom: canvasBox.Bottom,
}
}
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
xoffset += width + spacing
}
}
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
if !bc.XAxis.Hidden {
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
axisStyle.WriteToRenderer(r)
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
r.LineTo(canvasBox.Right, canvasBox.Bottom)
r.Stroke()
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
r.Stroke()
cursor := canvasBox.Left
for index, bar := range bc.Bars {
barLabelBox := Box{
Top: canvasBox.Bottom + DefaultXAxisMargin,
Left: cursor,
Right: cursor + width + spacing,
Bottom: bc.GetHeight(),
}
if len(bar.Label) > 0 {
Draw.TextWithin(r, bar.Label, barLabelBox, axisStyle)
}
axisStyle.WriteToRenderer(r)
if index < len(bc.Bars)-1 {
r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
r.Stroke()
}
cursor += width + spacing
}
}
}
func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
if !bc.YAxis.Style.Hidden {
bc.YAxis.Render(r, canvasBox, yr, bc.styleDefaultsAxes(), ticks)
}
}
func (bc BarChart) drawTitle(r Renderer) {
if len(bc.Title) > 0 && !bc.TitleStyle.Hidden {
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 {
return !bc.YAxis.Style.Hidden
}
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
yr.SetDomain(canvasBox.Height())
return yr
}
func (bc BarChart) getDefaultCanvasBox() Box {
return bc.box()
}
func (bc BarChart) getValueFormatters() ValueFormatter {
if bc.YAxis.ValueFormatter != nil {
return bc.YAxis.ValueFormatter
}
return FloatValueFormatter
}
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
if !bc.YAxis.Style.Hidden {
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
}
return
}
func (bc BarChart) calculateEffectiveBarSpacing(canvasBox Box) int {
totalWithBaseSpacing := bc.calculateTotalBarWidth(bc.GetBarWidth(), bc.GetBarSpacing())
if totalWithBaseSpacing > canvasBox.Width() {
lessBarWidths := canvasBox.Width() - (len(bc.Bars) * bc.GetBarWidth())
if lessBarWidths > 0 {
return int(math.Ceil(float64(lessBarWidths) / float64(len(bc.Bars))))
}
return 0
}
return bc.GetBarSpacing()
}
func (bc BarChart) calculateEffectiveBarWidth(canvasBox Box, spacing int) int {
totalWithBaseWidth := bc.calculateTotalBarWidth(bc.GetBarWidth(), spacing)
if totalWithBaseWidth > canvasBox.Width() {
totalLessBarSpacings := canvasBox.Width() - (len(bc.Bars) * spacing)
if totalLessBarSpacings > 0 {
return int(math.Ceil(float64(totalLessBarSpacings) / float64(len(bc.Bars))))
}
return 0
}
return bc.GetBarWidth()
}
func (bc BarChart) calculateTotalBarWidth(barWidth, spacing int) int {
return len(bc.Bars) * (barWidth + spacing)
}
func (bc BarChart) calculateScaledTotalWidth(canvasBox Box) (width, spacing, total int) {
spacing = bc.calculateEffectiveBarSpacing(canvasBox)
width = bc.calculateEffectiveBarWidth(canvasBox, spacing)
total = bc.calculateTotalBarWidth(width, spacing)
return
}
func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, yticks []Tick) Box {
axesOuterBox := canvasBox.Clone()
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
if !bc.XAxis.Hidden {
xaxisHeight := DefaultVerticalTickHeight
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
axisStyle.WriteToRenderer(r)
cursor := canvasBox.Left
for _, bar := range bc.Bars {
if len(bar.Label) > 0 {
barLabelBox := Box{
Top: canvasBox.Bottom + DefaultXAxisMargin,
Left: cursor,
Right: cursor + bc.GetBarWidth() + bc.GetBarSpacing(),
Bottom: bc.GetHeight(),
}
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
linesBox := Text.MeasureLines(r, lines, axisStyle)
xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
}
}
xbox := Box{
Top: canvasBox.Top,
Left: canvasBox.Left,
Right: canvasBox.Left + totalWidth,
Bottom: bc.GetHeight() - xaxisHeight,
}
axesOuterBox = axesOuterBox.Grow(xbox)
}
if !bc.YAxis.Style.Hidden {
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
return canvasBox.OuterConstrain(bc.box(), axesOuterBox)
}
// box returns the chart bounds as a box.
func (bc BarChart) box() Box {
dpr := bc.Background.Padding.GetRight(10)
dpb := bc.Background.Padding.GetBottom(50)
return Box{
Top: bc.Background.Padding.GetTop(20),
Left: bc.Background.Padding.GetLeft(20),
Right: bc.GetWidth() - dpr,
Bottom: bc.GetHeight() - dpb,
}
}
func (bc BarChart) getBackgroundStyle() Style {
return bc.Background.InheritFrom(bc.styleDefaultsBackground())
}
func (bc BarChart) styleDefaultsBackground() Style {
return Style{
FillColor: bc.GetColorPalette().BackgroundColor(),
StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (bc BarChart) styleDefaultsBar(index int) Style {
return Style{
StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
StrokeWidth: 3.0,
FillColor: bc.GetColorPalette().GetSeriesColor(index),
}
}
func (bc BarChart) styleDefaultsTitle() Style {
return bc.TitleStyle.InheritFrom(Style{
FontColor: bc.GetColorPalette().TextColor(),
Font: bc.GetFont(),
FontSize: bc.getTitleFontSize(),
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
})
}
func (bc BarChart) getTitleFontSize() float64 {
effectiveDimension := MinInt(bc.GetWidth(), bc.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
}
func (bc BarChart) styleDefaultsAxes() Style {
return Style{
StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
Font: bc.GetFont(),
FontSize: DefaultAxisFontSize,
FontColor: bc.GetColorPalette().TextColor(),
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
}
}
func (bc BarChart) styleDefaultsElements() Style {
return Style{
Font: bc.GetFont(),
}
}
// GetColorPalette returns the color palette for the chart.
func (bc BarChart) GetColorPalette() ColorPalette {
if bc.ColorPalette != nil {
return bc.ColorPalette
}
return AlternateColorPalette
}

310
bar_chart_test.go Normal file
View file

@ -0,0 +1,310 @@
package chart
import (
"bytes"
"math"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestBarChartRender(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Width: 1024,
Title: "Test Title",
Bars: []Value{
{Value: 1.0, Label: "One"},
{Value: 2.0, Label: "Two"},
{Value: 3.0, Label: "Three"},
{Value: 4.0, Label: "Four"},
{Value: 5.0, Label: "Five"},
},
}
buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf)
testutil.AssertNil(t, err)
testutil.AssertNotZero(t, buf.Len())
}
func TestBarChartRenderZero(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Width: 1024,
Title: "Test Title",
Bars: []Value{
{Value: 0.0, Label: "One"},
{Value: 0.0, Label: "Two"},
},
}
buf := bytes.NewBuffer([]byte{})
err := bc.Render(PNG, buf)
testutil.AssertNotNil(t, err)
}
func TestBarChartProps(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
testutil.AssertEqual(t, DefaultDPI, bc.GetDPI())
bc.DPI = 100
testutil.AssertEqual(t, 100, bc.GetDPI())
testutil.AssertNil(t, bc.GetFont())
f, err := GetDefaultFont()
testutil.AssertNil(t, err)
bc.Font = f
testutil.AssertNotNil(t, bc.GetFont())
testutil.AssertEqual(t, DefaultChartWidth, bc.GetWidth())
bc.Width = DefaultChartWidth - 1
testutil.AssertEqual(t, DefaultChartWidth-1, bc.GetWidth())
testutil.AssertEqual(t, DefaultChartHeight, bc.GetHeight())
bc.Height = DefaultChartHeight - 1
testutil.AssertEqual(t, DefaultChartHeight-1, bc.GetHeight())
testutil.AssertEqual(t, DefaultBarSpacing, bc.GetBarSpacing())
bc.BarSpacing = 150
testutil.AssertEqual(t, 150, bc.GetBarSpacing())
testutil.AssertEqual(t, DefaultBarWidth, bc.GetBarWidth())
bc.BarWidth = 75
testutil.AssertEqual(t, 75, bc.GetBarWidth())
}
func TestBarChartRenderNoBars(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
err := bc.Render(PNG, bytes.NewBuffer([]byte{}))
testutil.AssertNotNil(t, err)
}
func TestBarChartGetRanges(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
testutil.AssertEqual(t, -math.MaxFloat64, yr.GetMax())
testutil.AssertEqual(t, math.MaxFloat64, yr.GetMin())
}
func TestBarChartGetRangesBarsMinMax(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Bars: []Value{
{Value: 1.0},
{Value: 10.0},
},
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
testutil.AssertEqual(t, 10, yr.GetMax())
testutil.AssertEqual(t, 1, yr.GetMin())
}
func TestBarChartGetRangesMinMax(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
YAxis: YAxis{
Range: &ContinuousRange{
Min: 5.0,
Max: 15.0,
},
Ticks: []Tick{
{Value: 7.0, Label: "Foo"},
{Value: 11.0, Label: "Foo2"},
},
},
Bars: []Value{
{Value: 1.0},
{Value: 10.0},
},
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
testutil.AssertEqual(t, 15, yr.GetMax())
testutil.AssertEqual(t, 5, yr.GetMin())
}
func TestBarChartGetRangesTicksMinMax(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
YAxis: YAxis{
Ticks: []Tick{
{Value: 7.0, Label: "Foo"},
{Value: 11.0, Label: "Foo2"},
},
},
Bars: []Value{
{Value: 1.0},
{Value: 10.0},
},
}
yr := bc.getRanges()
testutil.AssertNotNil(t, yr)
testutil.AssertFalse(t, yr.IsZero())
testutil.AssertEqual(t, 11, yr.GetMax())
testutil.AssertEqual(t, 7, yr.GetMin())
}
func TestBarChartHasAxes(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
testutil.AssertTrue(t, bc.hasAxes())
bc.YAxis = YAxis{
Style: Hidden(),
}
testutil.AssertFalse(t, bc.hasAxes())
}
func TestBarChartGetDefaultCanvasBox(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
b := bc.getDefaultCanvasBox()
testutil.AssertFalse(t, b.IsZero())
}
func TestBarChartSetRangeDomains(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
cb := bc.box()
yr := bc.getRanges()
yr2 := bc.setRangeDomains(cb, yr)
testutil.AssertNotZero(t, yr2.GetDomain())
}
func TestBarChartGetValueFormatters(t *testing.T) {
// replaced new assertions helper
bc := BarChart{}
vf := bc.getValueFormatters()
testutil.AssertNotNil(t, vf)
testutil.AssertEqual(t, "1234.00", vf(1234.0))
bc.YAxis.ValueFormatter = func(_ interface{}) string { return "test" }
testutil.AssertEqual(t, "test", bc.getValueFormatters()(1234))
}
func TestBarChartGetAxesTicks(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Bars: []Value{
{Value: 1.0},
{Value: 2.0},
{Value: 3.0},
},
}
r, err := PNG(128, 128)
testutil.AssertNil(t, err)
yr := bc.getRanges()
yf := bc.getValueFormatters()
bc.YAxis.Style.Hidden = true
ticks := bc.getAxesTicks(r, yr, yf)
testutil.AssertEmpty(t, ticks)
bc.YAxis.Style.Hidden = false
ticks = bc.getAxesTicks(r, yr, yf)
testutil.AssertLen(t, ticks, 2)
}
func TestBarChartCalculateEffectiveBarSpacing(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Width: 1024,
BarWidth: 10,
Bars: []Value{
{Value: 1.0, Label: "One"},
{Value: 2.0, Label: "Two"},
{Value: 3.0, Label: "Three"},
{Value: 4.0, Label: "Four"},
{Value: 5.0, Label: "Five"},
},
}
spacing := bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertNotZero(t, spacing)
bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertZero(t, spacing)
}
func TestBarChartCalculateEffectiveBarWidth(t *testing.T) {
// replaced new assertions helper
bc := BarChart{
Width: 1024,
BarWidth: 10,
Bars: []Value{
{Value: 1.0, Label: "One"},
{Value: 2.0, Label: "Two"},
{Value: 3.0, Label: "Three"},
{Value: 4.0, Label: "Four"},
{Value: 5.0, Label: "Five"},
},
}
cb := bc.box()
spacing := bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertNotZero(t, spacing)
barWidth := bc.calculateEffectiveBarWidth(bc.box(), spacing)
testutil.AssertEqual(t, 10, barWidth)
bc.BarWidth = 250
spacing = bc.calculateEffectiveBarSpacing(bc.box())
testutil.AssertZero(t, spacing)
barWidth = bc.calculateEffectiveBarWidth(bc.box(), spacing)
testutil.AssertEqual(t, 199, barWidth)
testutil.AssertEqual(t, cb.Width()+1, bc.calculateTotalBarWidth(barWidth, spacing))
bw, bs, total := bc.calculateScaledTotalWidth(cb)
testutil.AssertEqual(t, spacing, bs)
testutil.AssertEqual(t, barWidth, bw)
testutil.AssertEqual(t, cb.Width()+1, total)
}
func TestBarChatGetTitleFontSize(t *testing.T) {
// replaced new assertions helper
size := BarChart{Width: 2049, Height: 2049}.getTitleFontSize()
testutil.AssertEqual(t, 48, size)
size = BarChart{Width: 1025, Height: 1025}.getTitleFontSize()
testutil.AssertEqual(t, 24, size)
size = BarChart{Width: 513, Height: 513}.getTitleFontSize()
testutil.AssertEqual(t, 18, size)
size = BarChart{Width: 257, Height: 257}.getTitleFontSize()
testutil.AssertEqual(t, 12, size)
size = BarChart{Width: 128, Height: 128}.getTitleFontSize()
testutil.AssertEqual(t, 10, size)
}

135
bollinger_band_series.go Normal file
View file

@ -0,0 +1,135 @@
package chart
import (
"fmt"
)
// Interface Assertions.
var (
_ Series = (*BollingerBandsSeries)(nil)
)
// BollingerBandsSeries draws bollinger bands for an inner series.
// Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev.
type BollingerBandsSeries struct {
Name string
Style Style
YAxis YAxisType
Period int
K float64
InnerSeries ValuesProvider
valueBuffer *ValueBuffer
}
// GetName returns the name of the time series.
func (bbs BollingerBandsSeries) GetName() string {
return bbs.Name
}
// GetStyle returns the line style.
func (bbs BollingerBandsSeries) GetStyle() Style {
return bbs.Style
}
// GetYAxis returns which YAxis the series draws on.
func (bbs BollingerBandsSeries) GetYAxis() YAxisType {
return bbs.YAxis
}
// GetPeriod returns the window size.
func (bbs BollingerBandsSeries) GetPeriod() int {
if bbs.Period == 0 {
return DefaultSimpleMovingAveragePeriod
}
return bbs.Period
}
// GetK returns the K value, or the number of standard deviations above and below
// to band the simple moving average with.
// Typical K value is 2.0.
func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
if bbs.K == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return 2.0
}
return bbs.K
}
// Len returns the number of elements in the series.
func (bbs BollingerBandsSeries) Len() int {
return bbs.InnerSeries.Len()
}
// GetBoundedValues gets the bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
if bbs.InnerSeries == nil {
return
}
if bbs.valueBuffer == nil || index == 0 {
bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
}
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
bbs.valueBuffer.Dequeue()
}
px, py := bbs.InnerSeries.GetValues(index)
bbs.valueBuffer.Enqueue(py)
x = px
ay := Seq{bbs.valueBuffer}.Average()
std := Seq{bbs.valueBuffer}.StdDev()
y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std)
return
}
// GetBoundedLastValues returns the last bounded value for the series.
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
if bbs.InnerSeries == nil {
return
}
period := bbs.GetPeriod()
seriesLength := bbs.InnerSeries.Len()
startAt := seriesLength - period
if startAt < 0 {
startAt = 0
}
vb := NewValueBufferWithCapacity(period)
for index := startAt; index < seriesLength; index++ {
xn, yn := bbs.InnerSeries.GetValues(index)
vb.Enqueue(yn)
x = xn
}
ay := Seq{vb}.Average()
std := Seq{vb}.StdDev()
y1 = ay + (bbs.GetK() * std)
y2 = ay - (bbs.GetK() * std)
return
}
// Render renders the series.
func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{
StrokeWidth: 1.0,
StrokeColor: DefaultAxisColor.WithAlpha(64),
FillColor: DefaultAxisColor.WithAlpha(32),
}))
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
}
// Validate validates the series.
func (bbs BollingerBandsSeries) Validate() error {
if bbs.InnerSeries == nil {
return fmt.Errorf("bollinger bands series requires InnerSeries to be set")
}
return nil
}

View file

@ -0,0 +1,52 @@
package chart
import (
"fmt"
"math"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestBollingerBandSeries(t *testing.T) {
// replaced new assertions helper
s1 := mockValuesProvider{
X: LinearRange(1.0, 100.0),
Y: RandomValuesWithMax(100, 1024),
}
bbs := &BollingerBandsSeries{
InnerSeries: s1,
}
xvalues := make([]float64, 100)
y1values := make([]float64, 100)
y2values := make([]float64, 100)
for x := 0; x < 100; x++ {
xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x)
}
for x := bbs.GetPeriod(); x < 100; x++ {
testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x]))
}
}
func TestBollingerBandLastValue(t *testing.T) {
// replaced new assertions helper
s1 := mockValuesProvider{
X: LinearRange(1.0, 100.0),
Y: LinearRange(1.0, 100.0),
}
bbs := &BollingerBandsSeries{
InnerSeries: s1,
}
x, y1, y2 := bbs.GetBoundedLastValues()
testutil.AssertEqual(t, 100.0, x)
testutil.AssertEqual(t, 101, math.Floor(y1))
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},
},
}
}

181
box.go
View file

@ -1,6 +1,25 @@
package chart package chart
import "fmt" import (
"fmt"
"math"
)
var (
// BoxZero is a preset box that represents an intentional zero value.
BoxZero = Box{IsSet: true}
)
// NewBox returns a new (set) box.
func NewBox(top, left, right, bottom int) Box {
return Box{
IsSet: true,
Top: top,
Left: left,
Right: right,
Bottom: bottom,
}
}
// Box represents the main 4 dimensions of a box. // Box represents the main 4 dimensions of a box.
type Box struct { type Box struct {
@ -8,10 +27,14 @@ type Box struct {
Left int Left int
Right int Right int
Bottom int Bottom int
IsSet bool
} }
// IsZero returns if the box is set or not. // IsZero returns if the box is set or not.
func (b Box) IsZero() bool { func (b Box) IsZero() bool {
if b.IsSet {
return false
}
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0 return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
} }
@ -22,7 +45,7 @@ func (b Box) String() string {
// GetTop returns a coalesced value with a default. // GetTop returns a coalesced value with a default.
func (b Box) GetTop(defaults ...int) int { func (b Box) GetTop(defaults ...int) int {
if b.Top == 0 { if !b.IsSet && b.Top == 0 {
if len(defaults) > 0 { if len(defaults) > 0 {
return defaults[0] return defaults[0]
} }
@ -33,7 +56,7 @@ func (b Box) GetTop(defaults ...int) int {
// GetLeft returns a coalesced value with a default. // GetLeft returns a coalesced value with a default.
func (b Box) GetLeft(defaults ...int) int { func (b Box) GetLeft(defaults ...int) int {
if b.Left == 0 { if !b.IsSet && b.Left == 0 {
if len(defaults) > 0 { if len(defaults) > 0 {
return defaults[0] return defaults[0]
} }
@ -44,7 +67,7 @@ func (b Box) GetLeft(defaults ...int) int {
// GetRight returns a coalesced value with a default. // GetRight returns a coalesced value with a default.
func (b Box) GetRight(defaults ...int) int { func (b Box) GetRight(defaults ...int) int {
if b.Right == 0 { if !b.IsSet && b.Right == 0 {
if len(defaults) > 0 { if len(defaults) > 0 {
return defaults[0] return defaults[0]
} }
@ -55,7 +78,7 @@ func (b Box) GetRight(defaults ...int) int {
// GetBottom returns a coalesced value with a default. // GetBottom returns a coalesced value with a default.
func (b Box) GetBottom(defaults ...int) int { func (b Box) GetBottom(defaults ...int) int {
if b.Bottom == 0 { if !b.IsSet && b.Bottom == 0 {
if len(defaults) > 0 { if len(defaults) > 0 {
return defaults[0] return defaults[0]
} }
@ -76,8 +99,8 @@ func (b Box) Height() int {
// Center returns the center of the box // Center returns the center of the box
func (b Box) Center() (x, y int) { func (b Box) Center() (x, y int) {
w, h := b.Width(), b.Height() w2, h2 := b.Width()>>1, b.Height()>>1
return b.Left + w>>1, b.Top + h>>1 return b.Left + w2, b.Top + h2
} }
// Aspect returns the aspect ratio of the box. // Aspect returns the aspect ratio of the box.
@ -88,6 +111,7 @@ func (b Box) Aspect() float64 {
// Clone returns a new copy of the box. // Clone returns a new copy of the box.
func (b Box) Clone() Box { func (b Box) Clone() Box {
return Box{ return Box{
IsSet: b.IsSet,
Top: b.Top, Top: b.Top,
Left: b.Left, Left: b.Left,
Right: b.Right, Right: b.Right,
@ -139,6 +163,16 @@ func (b Box) Shift(x, y int) Box {
} }
} }
// Corners returns the box as a set of corners.
func (b Box) Corners() BoxCorners {
return BoxCorners{
TopLeft: Point{b.Left, b.Top},
TopRight: Point{b.Right, b.Top},
BottomRight: Point{b.Right, b.Bottom},
BottomLeft: Point{b.Left, b.Bottom},
}
}
// Fit is functionally the inverse of grow. // Fit is functionally the inverse of grow.
// Fit maintains the original aspect ratio of the `other` box, // Fit maintains the original aspect ratio of the `other` box,
// but constrains it to the bounds of the target box. // but constrains it to the bounds of the target box.
@ -185,25 +219,12 @@ func (b Box) Fit(other Box) Box {
// more literally like the opposite of grow. // more literally like the opposite of grow.
func (b Box) Constrain(other Box) Box { func (b Box) Constrain(other Box) Box {
newBox := b.Clone() newBox := b.Clone()
if other.Top < b.Top {
delta := b.Top - other.Top
newBox.Top = other.Top + delta
}
if other.Left < b.Left { newBox.Top = MaxInt(newBox.Top, other.Top)
delta := b.Left - other.Left newBox.Left = MaxInt(newBox.Left, other.Left)
newBox.Left = other.Left + delta newBox.Right = MinInt(newBox.Right, other.Right)
} newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
if other.Right > b.Right {
delta := other.Right - b.Right
newBox.Right = other.Right - delta
}
if other.Bottom > b.Bottom {
delta := other.Bottom - b.Bottom
newBox.Bottom = other.Bottom - delta
}
return newBox return newBox
} }
@ -232,3 +253,115 @@ 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.
type BoxCorners struct {
TopLeft, TopRight, BottomRight, BottomLeft Point
}
// Box return the BoxCorners as a regular box.
func (bc BoxCorners) Box() Box {
return Box{
Top: MinInt(bc.TopLeft.Y, bc.TopRight.Y),
Left: MinInt(bc.TopLeft.X, bc.BottomLeft.X),
Right: MaxInt(bc.TopRight.X, bc.BottomRight.X),
Bottom: MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
}
}
// Width returns the width
func (bc BoxCorners) Width() int {
minLeft := MinInt(bc.TopLeft.X, bc.BottomLeft.X)
maxRight := MaxInt(bc.TopRight.X, bc.BottomRight.X)
return maxRight - minLeft
}
// Height returns the height
func (bc BoxCorners) Height() int {
minTop := MinInt(bc.TopLeft.Y, bc.TopRight.Y)
maxBottom := MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
return maxBottom - minTop
}
// Center returns the center of the box
func (bc BoxCorners) Center() (x, y int) {
left := MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
right := MeanInt(bc.TopRight.X, bc.BottomRight.X)
x = ((right - left) >> 1) + left
top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
y = ((bottom - top) >> 1) + top
return
}
// Rotate rotates the box.
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
cx, cy := bc.Center()
thetaRadians := DegreesToRadians(thetaDegrees)
tlx, tly := RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
trx, try := RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
brx, bry := RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
blx, bly := RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
return BoxCorners{
TopLeft: Point{tlx, tly},
TopRight: Point{trx, try},
BottomRight: Point{brx, bry},
BottomLeft: Point{blx, bly},
}
}
// Equals returns if the box equals another box.
func (bc BoxCorners) Equals(other BoxCorners) bool {
return bc.TopLeft.Equals(other.TopLeft) &&
bc.TopRight.Equals(other.TopRight) &&
bc.BottomRight.Equals(other.BottomRight) &&
bc.BottomLeft.Equals(other.BottomLeft)
}
func (bc BoxCorners) String() string {
return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
}
// Point is an X,Y pair
type Point struct {
X, Y int
}
// DistanceTo calculates the distance to another point.
func (p Point) DistanceTo(other Point) float64 {
dx := math.Pow(float64(p.X-other.X), 2)
dy := math.Pow(float64(p.Y-other.Y), 2)
return math.Pow(dx+dy, 0.5)
}
// Equals returns if a point equals another point.
func (p Point) Equals(other Point) bool {
return p.X == other.X && p.Y == other.Y
}
// String returns a string representation of the point.
func (p Point) String() string {
return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
}

View file

@ -4,105 +4,185 @@ 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) {
// replaced new assertions helper
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
cab := a.Constrain(b)
testutil.AssertEqual(t, 64, cab.Top)
testutil.AssertEqual(t, 64, cab.Left)
testutil.AssertEqual(t, 192, cab.Right)
testutil.AssertEqual(t, 170, cab.Bottom)
cac := a.Constrain(c)
testutil.AssertEqual(t, 64, cac.Top)
testutil.AssertEqual(t, 64, cac.Left)
testutil.AssertEqual(t, 170, cac.Right)
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 := Box{0, 0, 100, 100} box := NewBox(0, 0, 100, 100)
canvas := Box{5, 5, 95, 95} canvas := NewBox(5, 5, 95, 95)
taller := Box{-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 := Box{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) {
// replaced new assertions helper
b := Box{
Top: 5,
Left: 5,
Right: 10,
Bottom: 10,
}
shifted := b.Shift(1, 2)
testutil.AssertEqual(t, 7, shifted.Top)
testutil.AssertEqual(t, 6, shifted.Left)
testutil.AssertEqual(t, 11, shifted.Right)
testutil.AssertEqual(t, 12, shifted.Bottom)
}
func TestBoxCenter(t *testing.T) {
// replaced new assertions helper
b := Box{
Top: 10,
Left: 10,
Right: 20,
Bottom: 30,
}
cx, cy := b.Center()
testutil.AssertEqual(t, 15, cx)
testutil.AssertEqual(t, 20, cy)
}
func TestBoxCornersCenter(t *testing.T) {
// replaced new assertions helper
bc := BoxCorners{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
cx, cy := bc.Center()
testutil.AssertEqual(t, 10, cx)
testutil.AssertEqual(t, 10, cy)
}
func TestBoxCornersRotate(t *testing.T) {
// replaced new assertions helper
bc := BoxCorners{
TopLeft: Point{5, 5},
TopRight: Point{15, 5},
BottomRight: Point{15, 15},
BottomLeft: Point{5, 15},
}
rotated := bc.Rotate(45)
testutil.AssertTrue(t, rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
} }

344
chart.go
View file

@ -2,6 +2,7 @@ package chart
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"math" "math"
@ -13,6 +14,8 @@ type Chart struct {
Title string Title string
TitleStyle Style TitleStyle Style
ColorPalette ColorPalette
Width int Width int
Height int Height int
DPI float64 DPI float64
@ -29,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.
@ -69,8 +74,12 @@ func (c Chart) GetHeight() int {
// Render renders the chart with the given renderer to the given io.Writer. // Render renders the chart with the given renderer to the given io.Writer.
func (c Chart) Render(rp RendererProvider, w io.Writer) error { 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 err := c.checkHasVisibleSeries(); err != nil {
return err
}
c.YAxisSecondary.AxisType = YAxisSecondary c.YAxisSecondary.AxisType = YAxisSecondary
r, err := rp(c.GetWidth(), c.GetHeight()) r, err := rp(c.GetWidth(), c.GetHeight())
@ -93,32 +102,44 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
xr, yr, yra := c.getRanges() xr, yr, yra := c.getRanges()
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)
if err != nil { if err != nil {
r.Save(w)
return err return err
} }
if c.hasAxes() { if c.hasAxes() {
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.getAxisAdjustedCanvasBox(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.
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)
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
} }
if c.hasAnnotationSeries() { if c.hasAnnotationSeries() {
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)
c.drawAxes(r, canvasBox, xr, yr, yra, xt, yt, yta) c.drawAxes(r, canvasBox, xr, yr, yra, xt, yt, yta)
for index, series := range c.Series { for index, series := range c.Series {
c.drawSeries(r, canvasBox, xr, yr, yra, series, index) c.drawSeries(r, canvasBox, xr, yr, yra, series, index)
} }
c.drawTitle(r) c.drawTitle(r)
for _, a := range c.Elements { for _, a := range c.Elements {
@ -128,18 +149,65 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
return r.Save(w) return r.Save(w)
} }
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { func (c Chart) checkHasVisibleSeries() error {
var minx, maxx float64 = math.MaxFloat64, 0 var style Style
var miny, maxy float64 = math.MaxFloat64, 0
var minya, maxya float64 = math.MaxFloat64, 0
for _, s := range c.Series { for _, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show { style = s.GetStyle()
if !style.Hidden {
return nil
}
}
return fmt.Errorf("chart render; must have (1) visible series")
}
func (c Chart) validateSeries() error {
var err error
for _, s := range c.Series {
err = s.Validate()
if err != nil {
return err
}
}
return nil
}
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
var minx, maxx float64 = math.MaxFloat64, -math.MaxFloat64
var miny, maxy float64 = math.MaxFloat64, -math.MaxFloat64
var minya, maxya float64 = math.MaxFloat64, -math.MaxFloat64
seriesMappedToSecondaryAxis := false
// note: a possible future optimization is to not scan the series values if
// all axis are represented by either custom ticks or custom ranges.
for _, s := range c.Series {
if !s.GetStyle().Hidden {
seriesAxis := s.GetYAxis() seriesAxis := s.GetYAxis()
if vp, isValueProvider := s.(ValueProvider); isValueProvider { if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
seriesLength := bvp.Len()
for index := 0; index < seriesLength; index++ {
vx, vy1, vy2 := bvp.GetBoundedValues(index)
minx = math.Min(minx, vx)
maxx = math.Max(maxx, vx)
if seriesAxis == YAxisPrimary {
miny = math.Min(miny, vy1)
miny = math.Min(miny, vy2)
maxy = math.Max(maxy, vy1)
maxy = math.Max(maxy, vy2)
} else if seriesAxis == YAxisSecondary {
minya = math.Min(minya, vy1)
minya = math.Min(minya, vy2)
maxya = math.Max(maxya, vy1)
maxya = math.Max(maxya, vy2)
seriesMappedToSecondaryAxis = true
}
}
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
seriesLength := vp.Len() seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ { for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValue(index) vx, vy := vp.GetValues(index)
minx = math.Min(minx, vx) minx = math.Min(minx, vx)
maxx = math.Max(maxx, vx) maxx = math.Max(maxx, vx)
@ -150,52 +218,120 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
} else if seriesAxis == YAxisSecondary { } else if seriesAxis == YAxisSecondary {
minya = math.Min(minya, vy) minya = math.Min(minya, vy)
maxya = math.Max(maxya, vy) maxya = math.Max(maxya, vy)
seriesMappedToSecondaryAxis = true
} }
} }
} }
} }
} }
if !c.XAxis.Range.IsZero() { if c.XAxis.Range == nil {
xrange.Min = c.XAxis.Range.Min xrange = &ContinuousRange{}
xrange.Max = c.XAxis.Range.Max
} else { } else {
xrange.Min = minx xrange = c.XAxis.Range
xrange.Max = maxx
} }
if !c.YAxis.Range.IsZero() { if c.YAxis.Range == nil {
yrange.Min = c.YAxis.Range.Min yrange = &ContinuousRange{}
yrange.Max = c.YAxis.Range.Max
} else { } else {
yrange.Min = miny yrange = c.YAxis.Range
yrange.Max = maxy
yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds()
} }
if !c.YAxisSecondary.Range.IsZero() { if c.YAxisSecondary.Range == nil {
yrangeAlt.Min = c.YAxisSecondary.Range.Min yrangeAlt = &ContinuousRange{}
yrangeAlt.Max = c.YAxisSecondary.Range.Max
} else { } else {
yrangeAlt.Min = minya yrangeAlt = c.YAxisSecondary.Range
yrangeAlt.Max = maxya }
yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds()
if len(c.XAxis.Ticks) > 0 {
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
for _, t := range c.XAxis.Ticks {
tickMin = math.Min(tickMin, t.Value)
tickMax = math.Max(tickMax, t.Value)
}
xrange.SetMin(tickMin)
xrange.SetMax(tickMax)
} else if xrange.IsZero() {
xrange.SetMin(minx)
xrange.SetMax(maxx)
}
if len(c.YAxis.Ticks) > 0 {
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
for _, t := range c.YAxis.Ticks {
tickMin = math.Min(tickMin, t.Value)
tickMax = math.Max(tickMax, t.Value)
}
yrange.SetMin(tickMin)
yrange.SetMax(tickMax)
} else if yrange.IsZero() {
yrange.SetMin(miny)
yrange.SetMax(maxy)
if !c.YAxis.Style.Hidden {
delta := yrange.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
yrange.SetMin(rmin)
yrange.SetMax(rmax)
}
}
if len(c.YAxisSecondary.Ticks) > 0 {
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
for _, t := range c.YAxis.Ticks {
tickMin = math.Min(tickMin, t.Value)
tickMax = math.Max(tickMax, t.Value)
}
yrangeAlt.SetMin(tickMin)
yrangeAlt.SetMax(tickMax)
} else if seriesMappedToSecondaryAxis && yrangeAlt.IsZero() {
yrangeAlt.SetMin(minya)
yrangeAlt.SetMax(maxya)
if !c.YAxisSecondary.Style.Hidden {
delta := yrangeAlt.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
yrangeAlt.SetMin(rmin)
yrangeAlt.SetMax(rmax)
}
} }
return return
} }
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()
if math.IsInf(xDelta, 0) {
return errors.New("infinite x-range delta")
}
if math.IsNaN(xDelta) {
return errors.New("nan x-range delta")
}
if xDelta == 0 {
return errors.New("zero x-range delta; there needs to be at least (2) values")
}
if math.IsInf(xr.Delta(), 0) || math.IsNaN(xr.Delta()) { Debugf(c.Log, "checking yrange: %v", yr)
return errors.New("Invalid (infinite or NaN) x-range delta") yDelta := yr.GetDelta()
if math.IsInf(yDelta, 0) {
return errors.New("infinite y-range delta")
} }
if math.IsInf(yr.Delta(), 0) || math.IsNaN(yr.Delta()) { if math.IsNaN(yDelta) {
return errors.New("Invalid (infinite or NaN) y-range delta") return errors.New("nan y-range delta")
} }
if c.hasSecondarySeries() { if c.hasSecondarySeries() {
if math.IsInf(yra.Delta(), 0) || math.IsNaN(yra.Delta()) { Debugf(c.Log, "checking secondary yrange: %v", yra)
return errors.New("Invalid (infinite or NaN) y-secondary-range delta") yraDelta := yra.GetDelta()
if math.IsInf(yraDelta, 0) {
return errors.New("infinite secondary y-range delta")
}
if math.IsNaN(yraDelta) {
return errors.New("nan secondary y-range delta")
} }
} }
@ -203,17 +339,7 @@ func (c Chart) checkRanges(xr, yr, yra Range) error {
} }
func (c Chart) getDefaultCanvasBox() Box { func (c Chart) getDefaultCanvasBox() Box {
dpt := c.Background.Padding.GetTop(DefaultBackgroundPadding.Top) return c.Box()
dpl := c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left)
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: dpt,
Left: dpl,
Right: c.GetWidth() - dpr,
Bottom: c.GetHeight() - dpb,
}
} }
func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) { func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
@ -230,66 +356,66 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
} }
} }
if c.XAxis.ValueFormatter != nil { if c.XAxis.ValueFormatter != nil {
x = c.XAxis.ValueFormatter x = c.XAxis.GetValueFormatter()
} }
if c.YAxis.ValueFormatter != nil { if c.YAxis.ValueFormatter != nil {
y = c.YAxis.ValueFormatter y = c.YAxis.GetValueFormatter()
} }
if c.YAxisSecondary.ValueFormatter != nil { if c.YAxisSecondary.ValueFormatter != nil {
ya = c.YAxisSecondary.ValueFormatter ya = c.YAxisSecondary.GetValueFormatter()
} }
return return
} }
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.styleDefaultsAxis(), 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.styleDefaultsAxis(), 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.styleDefaultsAxis(), yfa) yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
} }
return return
} }
func (c Chart) getAxisAdjustedCanvasBox(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.styleDefaultsAxis(), 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.styleDefaultsAxis(), 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.styleDefaultsAxis(), 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)
} }
return canvasBox.OuterConstrain(c.Box(), axesOuterBox) return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
} }
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) { func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) {
xr2.Min, xr2.Max = xr.Min, xr.Max xr.SetDomain(canvasBox.Width())
xr2.Domain = canvasBox.Width() yr.SetDomain(canvasBox.Height())
yr2.Min, yr2.Max = yr.Min, yr.Max yra.SetDomain(canvasBox.Height())
yr2.Domain = canvasBox.Height() return xr, yr, yra
yra2.Min, yra2.Max = yra.Min, yra.Max
yra2.Domain = canvasBox.Height()
return
} }
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
} }
} }
@ -310,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 {
@ -327,28 +453,39 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
return canvasBox.OuterConstrain(c.Box(), annotationSeriesBox) return canvasBox.OuterConstrain(c.Box(), annotationSeriesBox)
} }
func (c Chart) getBackgroundStyle() Style {
return c.Background.InheritFrom(c.styleDefaultsBackground())
}
func (c Chart) drawBackground(r Renderer) { func (c Chart) drawBackground(r Renderer) {
DrawBox(r, c.Box(), c.Canvas.WithDefaultsFrom(c.styleDefaultsBackground())) Draw.Box(r, Box{
Right: c.GetWidth(),
Bottom: c.GetHeight(),
}, c.getBackgroundStyle())
}
func (c Chart) getCanvasStyle() Style {
return c.Canvas.InheritFrom(c.styleDefaultsCanvas())
} }
func (c Chart) drawCanvas(r Renderer, canvasBox Box) { func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
DrawBox(r, canvasBox, c.Canvas.WithDefaultsFrom(c.styleDefaultsCanvas())) Draw.Box(r, canvasBox, c.getCanvasStyle())
} }
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.styleDefaultsAxis(), 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.styleDefaultsAxis(), 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.styleDefaultsAxis(), 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 {
@ -358,9 +495,9 @@ 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(DefaultTextColor)) r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize) titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize) r.SetFontSize(titleFontSize)
@ -378,35 +515,36 @@ func (c Chart) drawTitle(r Renderer) {
func (c Chart) styleDefaultsBackground() Style { func (c Chart) styleDefaultsBackground() Style {
return Style{ return Style{
FillColor: DefaultBackgroundColor, FillColor: c.GetColorPalette().BackgroundColor(),
StrokeColor: DefaultBackgroundStrokeColor, StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
StrokeWidth: DefaultStrokeWidth, StrokeWidth: DefaultBackgroundStrokeWidth,
} }
} }
func (c Chart) styleDefaultsCanvas() Style { func (c Chart) styleDefaultsCanvas() Style {
return Style{ return Style{
FillColor: DefaultCanvasColor, FillColor: c.GetColorPalette().CanvasColor(),
StrokeColor: DefaultCanvasStrokeColor, StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultStrokeWidth, StrokeWidth: DefaultCanvasStrokeWidth,
} }
} }
func (c Chart) styleDefaultsSeries(seriesIndex int) Style { func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
return Style{ return Style{
StrokeColor: strokeColor, DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
StrokeWidth: DefaultStrokeWidth, StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
StrokeWidth: DefaultSeriesLineWidth,
Font: c.GetFont(), Font: c.GetFont(),
FontSize: DefaultFontSize, FontSize: DefaultFontSize,
} }
} }
func (c Chart) styleDefaultsAxis() Style { func (c Chart) styleDefaultsAxes() Style {
return Style{ return Style{
Font: c.GetFont(), Font: c.GetFont(),
FontColor: c.GetColorPalette().TextColor(),
FontSize: DefaultAxisFontSize, FontSize: DefaultAxisFontSize,
StrokeColor: DefaultAxisColor, StrokeColor: c.GetColorPalette().AxisStrokeColor(),
StrokeWidth: DefaultAxisLineWidth, StrokeWidth: DefaultAxisLineWidth,
} }
} }
@ -417,7 +555,23 @@ func (c Chart) styleDefaultsElements() Style {
} }
} }
// GetColorPalette returns the color palette for the chart.
func (c Chart) GetColorPalette() ColorPalette {
if c.ColorPalette != nil {
return c.ColorPalette
}
return DefaultColorPalette
}
// Box returns the chart bounds as a box. // Box returns the chart bounds as a box.
func (c Chart) Box() Box { func (c Chart) Box() Box {
return Box{Right: c.GetWidth(), Bottom: c.GetHeight()} dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
Left: c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
Right: c.GetWidth() - dpr,
Bottom: c.GetHeight() - dpb,
}
} }

View file

@ -2,60 +2,63 @@ package chart
import ( import (
"bytes" "bytes"
"image"
"image/png"
"math" "math"
"testing" "testing"
"time" "time"
"github.com/blendlabs/go-assert" "git.smarteching.com/zeni/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
) )
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{
@ -76,24 +79,24 @@ func TestChartGetRanges(t *testing.T) {
} }
xrange, yrange, yrangeAlt := c.getRanges() xrange, yrange, yrangeAlt := c.getRanges()
assert.Equal(-2.0, xrange.Min) testutil.AssertEqual(t, -2.0, xrange.GetMin())
assert.Equal(5.0, xrange.Max) testutil.AssertEqual(t, 5.0, xrange.GetMax())
assert.Equal(-2.1, yrange.Min) testutil.AssertEqual(t, -2.1, yrange.GetMin())
assert.Equal(4.5, yrange.Max) testutil.AssertEqual(t, 4.5, yrange.GetMax())
assert.Equal(10.0, yrangeAlt.Min) testutil.AssertEqual(t, 10.0, yrangeAlt.GetMin())
assert.Equal(14.0, yrangeAlt.Max) testutil.AssertEqual(t, 14.0, yrangeAlt.GetMax())
cSet := Chart{ cSet := Chart{
XAxis: XAxis{ XAxis: XAxis{
Range: Range{Min: 9.8, Max: 19.8}, Range: &ContinuousRange{Min: 9.8, Max: 19.8},
}, },
YAxis: YAxis{ YAxis: YAxis{
Range: Range{Min: 9.9, Max: 19.9}, Range: &ContinuousRange{Min: 9.9, Max: 19.9},
}, },
YAxisSecondary: YAxis{ YAxisSecondary: YAxis{
Range: Range{Min: 9.7, Max: 19.7}, Range: &ContinuousRange{Min: 9.7, Max: 19.7},
}, },
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
@ -113,26 +116,114 @@ func TestChartGetRanges(t *testing.T) {
} }
xr2, yr2, yra2 := cSet.getRanges() xr2, yr2, yra2 := cSet.getRanges()
assert.Equal(9.8, xr2.Min) testutil.AssertEqual(t, 9.8, xr2.GetMin())
assert.Equal(19.8, xr2.Max) testutil.AssertEqual(t, 19.8, xr2.GetMax())
assert.Equal(9.9, yr2.Min) testutil.AssertEqual(t, 9.9, yr2.GetMin())
assert.Equal(19.9, yr2.Max) testutil.AssertEqual(t, 19.9, yr2.GetMax())
assert.Equal(9.7, yra2.Min) testutil.AssertEqual(t, 9.7, yra2.GetMin())
assert.Equal(19.7, yra2.Max) testutil.AssertEqual(t, 19.7, yra2.GetMax())
}
func TestChartGetRangesUseTicks(t *testing.T) {
// replaced new assertions helper
// this test asserts that ticks should supercede manual ranges when generating the overall ranges.
c := Chart{
YAxis: YAxis{
Ticks: []Tick{
{0.0, "Zero"},
{1.0, "1.0"},
{2.0, "2.0"},
{3.0, "3.0"},
{4.0, "4.0"},
{5.0, "Five"},
},
Range: &ContinuousRange{
Min: -5.0,
Max: 5.0,
},
},
Series: []Series{
ContinuousSeries{
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
},
},
}
xr, yr, yar := c.getRanges()
testutil.AssertEqual(t, -2.0, xr.GetMin())
testutil.AssertEqual(t, 2.0, xr.GetMax())
testutil.AssertEqual(t, 0.0, yr.GetMin())
testutil.AssertEqual(t, 5.0, yr.GetMax())
testutil.AssertTrue(t, yar.IsZero(), yar.String())
}
func TestChartGetRangesUseUserRanges(t *testing.T) {
// replaced new assertions helper
c := Chart{
YAxis: YAxis{
Range: &ContinuousRange{
Min: -5.0,
Max: 5.0,
},
},
Series: []Series{
ContinuousSeries{
XValues: []float64{-2.0, -1.0, 0, 1.0, 2.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 4.5},
},
},
}
xr, yr, yar := c.getRanges()
testutil.AssertEqual(t, -2.0, xr.GetMin())
testutil.AssertEqual(t, 2.0, xr.GetMax())
testutil.AssertEqual(t, -5.0, yr.GetMin())
testutil.AssertEqual(t, 5.0, yr.GetMax())
testutil.AssertTrue(t, yar.IsZero(), yar.String())
}
func TestChartGetBackgroundStyle(t *testing.T) {
// replaced new assertions helper
c := Chart{
Background: Style{
FillColor: drawing.ColorBlack,
},
}
bs := c.getBackgroundStyle()
testutil.AssertEqual(t, bs.FillColor.String(), drawing.ColorBlack.String())
}
func TestChartGetCanvasStyle(t *testing.T) {
// replaced new assertions helper
c := Chart{
Canvas: Style{
FillColor: drawing.ColorBlack,
},
}
bs := c.getCanvasStyle()
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{
@ -145,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{
@ -174,81 +265,91 @@ 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: Range{Min: 9.8, Max: 19.8},
}, },
YAxis: YAxis{ YAxis: YAxis{
Style: Style{Show: true}, Range: &ContinuousRange{Min: 9.9, Max: 19.9},
Range: Range{Min: 9.9, Max: 19.9},
}, },
YAxisSecondary: YAxis{ YAxisSecondary: YAxis{
Style: Style{Show: true}, Range: &ContinuousRange{Min: 9.7, Max: 19.7},
Range: Range{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!",
Width: 1024, Width: 1024,
Height: 400, Height: 400,
YAxis: YAxis{ YAxis: YAxis{
Range: Range{ Range: &ContinuousRange{
Min: 0.0, Min: 0.0,
Max: 4.0, Max: 4.0,
}, },
@ -264,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{
@ -280,27 +381,214 @@ 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{
Range: Range{ Range: &ContinuousRange{
Min: math.Inf(-1), Min: math.Inf(-1),
Max: math.Inf(1), // this could really happen? eh. Max: math.Inf(1), // this could really happen? eh.
}, },
}, },
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: Seq(1.0, 10.0), XValues: LinearRange(1.0, 10.0),
YValues: Seq(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) {
// replaced new assertions helper
c := Chart{
Series: []Series{
ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
},
},
}
testutil.AssertNil(t, c.validateSeries())
c = Chart{
Series: []Series{
ContinuousSeries{
XValues: LinearRange(1.0, 10.0),
},
},
}
testutil.AssertNotNil(t, c.validateSeries())
}
func TestChartCheckRanges(t *testing.T) {
// replaced new assertions helper
c := Chart{
Series: []Series{
ContinuousSeries{
XValues: []float64{1.0, 2.0},
YValues: []float64{3.10, 3.14},
},
},
}
xr, yr, yra := c.getRanges()
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
}
func TestChartCheckRangesWithRanges(t *testing.T) {
// replaced new assertions helper
c := Chart{
XAxis: XAxis{
Range: &ContinuousRange{
Min: 0,
Max: 10,
},
},
YAxis: YAxis{
Range: &ContinuousRange{
Min: 0,
Max: 5,
},
},
Series: []Series{
ContinuousSeries{
XValues: []float64{1.0, 2.0},
YValues: []float64{3.14, 3.14},
},
},
}
xr, yr, yra := c.getRanges()
testutil.AssertNil(t, c.checkRanges(xr, yr, yra))
}
func at(i image.Image, x, y int) drawing.Color {
return drawing.ColorFromAlphaMixedRGBA(i.At(x, y).RGBA())
}
func TestChartE2ELine(t *testing.T) {
// replaced new assertions helper
c := Chart{
Height: 50,
Width: 50,
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Canvas: Style{
Padding: BoxZero,
},
Background: Style{
Padding: BoxZero,
},
Series: []Series{
ContinuousSeries{
XValues: LinearRangeWithStep(0, 4, 1),
YValues: LinearRangeWithStep(0, 4, 1),
},
},
}
var buffer = &bytes.Buffer{}
err := c.Render(PNG, buffer)
testutil.AssertNil(t, err)
// do color tests ...
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.ColorWhite, at(i, 49, 49))
// test a line mid point
defaultSeriesColor := GetDefaultColor(0)
testutil.AssertEqual(t, defaultSeriesColor, at(i, 0, 49))
testutil.AssertEqual(t, defaultSeriesColor, at(i, 49, 0))
testutil.AssertEqual(t, drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
}
func TestChartE2ELineWithFill(t *testing.T) {
// replaced new assertions helper
logBuffer := new(bytes.Buffer)
c := Chart{
Height: 50,
Width: 50,
Canvas: Style{
Padding: BoxZero,
},
Background: Style{
Padding: BoxZero,
},
TitleStyle: Hidden(),
XAxis: HideXAxis(),
YAxis: HideYAxis(),
YAxisSecondary: HideYAxis(),
Series: []Series{
ContinuousSeries{
Style: Style{
StrokeColor: drawing.ColorBlue,
FillColor: drawing.ColorRed,
},
XValues: LinearRangeWithStep(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 imgContent bytes.Buffer
err := poc.Render(PNG, &imgContent)
testutil.AssertNotNil(t, err)
} }

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

184
colors.go Normal file
View file

@ -0,0 +1,184 @@
package chart
import "git.smarteching.com/zeni/go-chart/v2/drawing"
var (
// ColorWhite is white.
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// ColorBlue is the basic theme blue color.
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
// ColorCyan is the basic theme cyan color.
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
// ColorGreen is the basic theme green color.
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
// ColorRed is the basic theme red color.
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
// ColorOrange is the basic theme orange color.
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
// ColorYellow is the basic theme yellow color.
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
// ColorBlack is the basic theme black color.
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
// ColorLightGray is the basic theme light gray color.
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
// ColorAlternateBlue is a alternate theme color.
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
// ColorAlternateGreen is a alternate theme color.
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
// ColorAlternateGray is a alternate theme color.
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
// ColorAlternateYellow is a alternate theme color.
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
// ColorAlternateLightGray is a alternate theme color.
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
// ColorTransparent is a transparent (alpha zero) color.
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
)
var (
// DefaultBackgroundColor is the default chart background color.
// It is equivalent to css color:white.
DefaultBackgroundColor = ColorWhite
// DefaultBackgroundStrokeColor is the default chart border color.
// It is equivalent to color:white.
DefaultBackgroundStrokeColor = ColorWhite
// DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white.
DefaultCanvasColor = ColorWhite
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
// It is equivalent to css color:white.
DefaultCanvasStrokeColor = ColorWhite
// DefaultTextColor is the default chart text color.
// It is equivalent to #333333.
DefaultTextColor = ColorBlack
// DefaultAxisColor is the default chart axis line color.
// It is equivalent to #333333.
DefaultAxisColor = ColorBlack
// DefaultStrokeColor is the default chart border color.
// It is equivalent to #efefef.
DefaultStrokeColor = ColorLightGray
// DefaultFillColor is the default fill color.
// It is equivalent to #0074d9.
DefaultFillColor = ColorBlue
// DefaultAnnotationFillColor is the default annotation background color.
DefaultAnnotationFillColor = ColorWhite
// DefaultGridLineColor is the default grid line color.
DefaultGridLineColor = ColorLightGray
)
var (
// DefaultColors are a couple default series colors.
DefaultColors = []drawing.Color{
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
// DefaultAlternateColors are a couple alternate colors.
DefaultAlternateColors = []drawing.Color{
ColorAlternateBlue,
ColorAlternateGreen,
ColorAlternateGray,
ColorAlternateYellow,
ColorBlue,
ColorGreen,
ColorRed,
ColorCyan,
ColorOrange,
}
)
// GetDefaultColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetDefaultColor(index int) drawing.Color {
finalIndex := index % len(DefaultColors)
return DefaultColors[finalIndex]
}
// GetAlternateColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).
func GetAlternateColor(index int) drawing.Color {
finalIndex := index % len(DefaultAlternateColors)
return DefaultAlternateColors[finalIndex]
}
// ColorPalette is a set of colors that.
type ColorPalette interface {
BackgroundColor() drawing.Color
BackgroundStrokeColor() drawing.Color
CanvasColor() drawing.Color
CanvasStrokeColor() drawing.Color
AxisStrokeColor() drawing.Color
TextColor() drawing.Color
GetSeriesColor(index int) drawing.Color
}
// DefaultColorPalette represents the default palatte.
var DefaultColorPalette defaultColorPalette
type defaultColorPalette struct{}
func (dp defaultColorPalette) BackgroundColor() drawing.Color {
return DefaultBackgroundColor
}
func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
return DefaultBackgroundStrokeColor
}
func (dp defaultColorPalette) CanvasColor() drawing.Color {
return DefaultCanvasColor
}
func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
return DefaultCanvasStrokeColor
}
func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
return DefaultAxisColor
}
func (dp defaultColorPalette) TextColor() drawing.Color {
return DefaultTextColor
}
func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
return GetDefaultColor(index)
}
// AlternateColorPalette represents the default palatte.
var AlternateColorPalette alternateColorPalette
type alternateColorPalette struct{}
func (ap alternateColorPalette) BackgroundColor() drawing.Color {
return DefaultBackgroundColor
}
func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
return DefaultBackgroundStrokeColor
}
func (ap alternateColorPalette) CanvasColor() drawing.Color {
return DefaultCanvasColor
}
func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
return DefaultCanvasStrokeColor
}
func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
return DefaultAxisColor
}
func (ap alternateColorPalette) TextColor() drawing.Color {
return DefaultTextColor
}
func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
return GetAlternateColor(index)
}

44
concat_series.go Normal file
View file

@ -0,0 +1,44 @@
package chart
// ConcatSeries is a special type of series that concatenates its `InnerSeries`.
type ConcatSeries []Series
// Len returns the length of the concatenated set of series.
func (cs ConcatSeries) Len() int {
total := 0
for _, s := range cs {
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
total += typed.Len()
}
}
return total
}
// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1)
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
cursor := 0
for _, s := range cs {
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
len := typed.Len()
if index < cursor+len {
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
return
}
cursor += typed.Len()
}
}
return
}
// Validate validates the series.
func (cs ConcatSeries) Validate() error {
var err error
for _, s := range cs {
err = s.Validate()
if err != nil {
return err
}
}
return nil
}

41
concat_series_test.go Normal file
View file

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

81
continuous_range.go Normal file
View file

@ -0,0 +1,81 @@
package chart
import (
"fmt"
"math"
)
// ContinuousRange represents a boundary for a set of numbers.
type ContinuousRange struct {
Min float64
Max float64
Domain int
Descending bool
}
// IsDescending returns if the range is descending.
func (r ContinuousRange) IsDescending() bool {
return r.Descending
}
// IsZero returns if the ContinuousRange has been set or not.
func (r ContinuousRange) IsZero() bool {
return (r.Min == 0 || math.IsNaN(r.Min)) &&
(r.Max == 0 || math.IsNaN(r.Max)) &&
r.Domain == 0
}
// GetMin gets the min value for the continuous range.
func (r ContinuousRange) GetMin() float64 {
return r.Min
}
// SetMin sets the min value for the continuous range.
func (r *ContinuousRange) SetMin(min float64) {
r.Min = min
}
// GetMax returns the max value for the continuous range.
func (r ContinuousRange) GetMax() float64 {
return r.Max
}
// SetMax sets the max value for the continuous range.
func (r *ContinuousRange) SetMax(max float64) {
r.Max = max
}
// GetDelta returns the difference between the min and max value.
func (r ContinuousRange) GetDelta() float64 {
return r.Max - r.Min
}
// GetDomain returns the range domain.
func (r ContinuousRange) GetDomain() int {
return r.Domain
}
// SetDomain sets the range domain.
func (r *ContinuousRange) SetDomain(domain int) {
r.Domain = domain
}
// String returns a simple string for the ContinuousRange.
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)
}
// Translate maps a given value into the ContinuousRange space.
func (r ContinuousRange) Translate(value float64) int {
normalized := value - r.Min
ratio := normalized / r.GetDelta()
if r.IsDescending() {
return r.Domain - int(math.Ceil(ratio*float64(r.Domain)))
}
return int(math.Ceil(ratio * float64(r.Domain)))
}

22
continuous_range_test.go Normal file
View file

@ -0,0 +1,22 @@
package chart
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestRangeTranslate(t *testing.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}
r := ContinuousRange{Domain: 1000}
r.Min, r.Max = MinMax(values...)
// delta = ~7.0
// value = ~5.0
// domain = ~1000
// 5/8 * 1000 ~=
testutil.AssertEqual(t, 0, r.Translate(1.0))
testutil.AssertEqual(t, 1000, r.Translate(8.0))
testutil.AssertEqual(t, 572, r.Translate(5.0))
}

View file

@ -1,5 +1,14 @@
package chart package chart
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
@ -7,6 +16,9 @@ type ContinuousSeries struct {
YAxis YAxisType YAxis YAxisType
XValueFormatter ValueFormatter
YValueFormatter ValueFormatter
XValues []float64 XValues []float64
YValues []float64 YValues []float64
} }
@ -26,15 +38,33 @@ func (cs ContinuousSeries) Len() int {
return len(cs.XValues) return len(cs.XValues)
} }
// GetValue gets a value at a given index. // GetValues gets the x,y values at a given index.
func (cs ContinuousSeries) GetValue(index int) (float64, float64) { 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.
func (cs ContinuousSeries) GetLastValues() (float64, float64) {
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
}
// GetValueFormatters returns value formatter defaults for the series. // GetValueFormatters returns value formatter defaults for the series.
func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) { func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
x = FloatValueFormatter if cs.XValueFormatter != nil {
y = FloatValueFormatter x = cs.XValueFormatter
} else {
x = FloatValueFormatter
}
if cs.YValueFormatter != nil {
y = cs.YValueFormatter
} else {
y = FloatValueFormatter
}
return return
} }
@ -45,6 +75,22 @@ func (cs ContinuousSeries) GetYAxis() YAxisType {
// Render renders the series. // Render renders the series.
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := cs.Style.WithDefaultsFrom(defaults) style := cs.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, cs) Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
}
// Validate validates the series.
func (cs ContinuousSeries) Validate() error {
if len(cs.XValues) == 0 {
return fmt.Errorf("continuous series; must have xvalues set")
}
if len(cs.YValues) == 0 {
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
} }

72
continuous_series_test.go Normal file
View file

@ -0,0 +1,72 @@
package chart
import (
"fmt"
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
func TestContinuousSeries(t *testing.T) {
// replaced new assertions helper
cs := ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
}
testutil.AssertEqual(t, "Test Series", cs.GetName())
testutil.AssertEqual(t, 10, cs.Len())
x0, y0 := cs.GetValues(0)
testutil.AssertEqual(t, 1.0, x0)
testutil.AssertEqual(t, 1.0, y0)
xn, yn := cs.GetValues(9)
testutil.AssertEqual(t, 10.0, xn)
testutil.AssertEqual(t, 10.0, yn)
xn, yn = cs.GetLastValues()
testutil.AssertEqual(t, 10.0, xn)
testutil.AssertEqual(t, 10.0, yn)
}
func TestContinuousSeriesValueFormatter(t *testing.T) {
// replaced new assertions helper
cs := ContinuousSeries{
XValueFormatter: func(v interface{}) string {
return fmt.Sprintf("%f foo", v)
},
YValueFormatter: func(v interface{}) string {
return fmt.Sprintf("%f bar", v)
},
}
xf, yf := cs.GetValueFormatters()
testutil.AssertEqual(t, "0.100000 foo", xf(0.1))
testutil.AssertEqual(t, "0.100000 bar", yf(0.1))
}
func TestContinuousSeriesValidate(t *testing.T) {
// replaced new assertions helper
cs := ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
YValues: LinearRange(1.0, 10.0),
}
testutil.AssertNil(t, cs.Validate())
cs = ContinuousSeries{
Name: "Test Series",
XValues: LinearRange(1.0, 10.0),
}
testutil.AssertNotNil(t, cs.Validate())
cs = ContinuousSeries{
Name: "Test Series",
YValues: LinearRange(1.0, 10.0),
}
testutil.AssertNotNil(t, cs.Validate())
}

View file

@ -1,19 +1,16 @@
package chart package chart
import (
"sync"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/drawing"
)
const ( const (
// DefaultChartHeight is the default chart height. // DefaultChartHeight is the default chart height.
DefaultChartHeight = 400 DefaultChartHeight = 400
// DefaultChartWidth is the default chart width. // DefaultChartWidth is the default chart width.
DefaultChartWidth = 200 DefaultChartWidth = 1024
// DefaultStrokeWidth is the default chart line/stroke width. // DefaultStrokeWidth is the default chart stroke width.
DefaultStrokeWidth = 1.0 DefaultStrokeWidth = 0.0
// DefaultDotWidth is the default chart dot width.
DefaultDotWidth = 0.0
// DefaultSeriesLineWidth is the default line width.
DefaultSeriesLineWidth = 1.0
// DefaultAxisLineWidth is the line width of the axis lines. // DefaultAxisLineWidth is the line width of the axis lines.
DefaultAxisLineWidth = 1.0 DefaultAxisLineWidth = 1.0
//DefaultDPI is the default dots per inch for the chart. //DefaultDPI is the default dots per inch for the chart.
@ -33,6 +30,14 @@ const (
// DefaultTitleTop is the default distance from the top of the chart to put the title. // DefaultTitleTop is the default distance from the top of the chart to put the title.
DefaultTitleTop = 10 DefaultTitleTop = 10
// DefaultBackgroundStrokeWidth is the default stroke on the chart background.
DefaultBackgroundStrokeWidth = 0.0
// DefaultCanvasStrokeWidth is the default stroke on the chart canvas.
DefaultCanvasStrokeWidth = 0.0
// DefaultLineSpacing is the default vertical distance between lines of text.
DefaultLineSpacing = 5
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels. // DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
DefaultYAxisMargin = 10 DefaultYAxisMargin = 10
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels. // DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
@ -57,50 +62,17 @@ const (
DefaultDateFormat = "2006-01-02" DefaultDateFormat = "2006-01-02"
// DefaultDateHourFormat is the date format for hour timestamp formats. // DefaultDateHourFormat is the date format for hour timestamp formats.
DefaultDateHourFormat = "01-02 3PM" DefaultDateHourFormat = "01-02 3PM"
// DefaultDateMinuteFormat is the date format for minute range timestamp formats.
DefaultDateMinuteFormat = "01-02 3:04PM"
// DefaultFloatFormat is the default float format. // DefaultFloatFormat is the default float format.
DefaultFloatFormat = "%.2f" DefaultFloatFormat = "%.2f"
// DefaultPercentValueFormat is the default percent format. // DefaultPercentValueFormat is the default percent format.
DefaultPercentValueFormat = "%0.2f%%" DefaultPercentValueFormat = "%0.2f%%"
)
var ( // DefaultBarSpacing is the default pixel spacing between bars.
// DefaultBackgroundColor is the default chart background color. DefaultBarSpacing = 100
// It is equivalent to css color:white. // DefaultBarWidth is the default pixel width of bars in a bar chart.
DefaultBackgroundColor = drawing.Color{R: 255, G: 255, B: 255, A: 255} DefaultBarWidth = 50
// DefaultBackgroundStrokeColor is the default chart border color.
// It is equivalent to color:white.
DefaultBackgroundStrokeColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white.
DefaultCanvasColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
// It is equivalent to css color:white.
DefaultCanvasStrokeColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultTextColor is the default chart text color.
// It is equivalent to #333333.
DefaultTextColor = drawing.Color{R: 51, G: 51, B: 51, A: 255}
// DefaultAxisColor is the default chart axis line color.
// It is equivalent to #333333.
DefaultAxisColor = drawing.Color{R: 51, G: 51, B: 51, A: 255}
// DefaultStrokeColor is the default chart border color.
// It is equivalent to #efefef.
DefaultStrokeColor = drawing.Color{R: 239, G: 239, B: 239, A: 255}
// DefaultFillColor is the default fill color.
// It is equivalent to #0074d9.
DefaultFillColor = drawing.Color{R: 0, G: 217, B: 116, A: 255}
// DefaultAnnotationFillColor is the default annotation background color.
DefaultAnnotationFillColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultGridLineColor is the default grid line color.
DefaultGridLineColor = drawing.Color{R: 239, G: 239, B: 239, A: 255}
)
var (
// DefaultSeriesStrokeColors are a couple default series colors.
DefaultSeriesStrokeColors = []drawing.Color{
drawing.Color{R: 0, G: 116, B: 217, A: 255},
drawing.Color{R: 0, G: 217, B: 116, A: 255},
drawing.Color{R: 217, G: 0, B: 116, A: 255},
}
) )
var ( var (
@ -114,37 +86,18 @@ var (
DashArrayDashesLarge = []int{10, 10} DashArrayDashesLarge = []int{10, 10}
) )
// GetDefaultSeriesStrokeColor returns a color from the default list by index.
// NOTE: the index will wrap around (using a modulo).g
func GetDefaultSeriesStrokeColor(index int) drawing.Color {
finalIndex := index % len(DefaultSeriesStrokeColors)
return DefaultSeriesStrokeColors[finalIndex]
}
var ( var (
// DefaultAnnotationPadding is the padding around an annotation. // DefaultAnnotationPadding is the padding around an annotation.
DefaultAnnotationPadding = Box{Top: 3, Left: 5, Right: 5, Bottom: 5} DefaultAnnotationPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
// DefaultBackgroundPadding is the default canvas padding config. // DefaultBackgroundPadding is the default canvas padding config.
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5} DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
) )
var ( const (
_defaultFontLock sync.Mutex // ContentTypePNG is the png mime type.
_defaultFont *truetype.Font ContentTypePNG = "image/png"
)
// GetDefaultFont returns the default font (Roboto-Medium). // ContentTypeSVG is the svg mime type.
func GetDefaultFont() (*truetype.Font, error) { ContentTypeSVG = "image/svg+xml"
if _defaultFont == nil { )
_defaultFontLock.Lock()
defer _defaultFontLock.Unlock()
if _defaultFont == nil {
font, err := truetype.Parse(roboto)
if err != nil {
return nil, err
}
_defaultFont = font
}
}
return _defaultFont, nil
}

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

325
draw.go Normal file
View file

@ -0,0 +1,325 @@
package chart
import (
"math"
)
var (
// Draw contains helpers for drawing common objects.
Draw = &draw{}
)
type draw struct{}
// LineSeries draws a line series with a renderer.
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
if vs.Len() == 0 {
return
}
cb := canvasBox.Bottom
cl := canvasBox.Left
v0x, v0y := vs.GetValues(0)
x0 := cl + xrange.Translate(v0x)
y0 := cb - yrange.Translate(v0y)
yv0 := yrange.Translate(0)
var vx, vy float64
var x, y int
if style.ShouldDrawStroke() && style.ShouldDrawFill() {
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
}
r.LineTo(x, MinInt(cb, cb-yv0))
r.LineTo(x0, MinInt(cb, cb-yv0))
r.LineTo(x0, y0)
r.Fill()
}
if style.ShouldDrawStroke() {
style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
}
r.Stroke()
}
if style.ShouldDrawDot() {
defaultDotWidth := style.GetDotWidth()
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
for i := 0; i < vs.Len(); i++ {
vx, vy = vs.GetValues(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
dotWidth := defaultDotWidth
if style.DotWidthProvider != nil {
dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
}
if style.DotColorProvider != nil {
dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
r.SetFillColor(dotColor)
r.SetStrokeColor(dotColor)
}
r.Circle(dotWidth, x, y)
r.FillStroke()
}
}
}
// BoundedSeries draws a series that implements BoundedValuesProvider.
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
drawOffsetIndex := 0
if len(drawOffsetIndexes) > 0 {
drawOffsetIndex = drawOffsetIndexes[0]
}
cb := canvasBox.Bottom
cl := canvasBox.Left
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
x0 := cl + xrange.Translate(v0x)
y0 := cb - yrange.Translate(v0y1)
var vx, vy1, vy2 float64
var x, y int
xvalues := make([]float64, bbs.Len())
xvalues[0] = v0x
y2values := make([]float64, bbs.Len())
y2values[0] = v0y2
style.GetFillAndStrokeOptions().WriteToRenderer(r)
r.MoveTo(x0, y0)
for i := 1; i < bbs.Len(); i++ {
vx, vy1, vy2 = bbs.GetBoundedValues(i)
xvalues[i] = vx
y2values[i] = vy2
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy1)
if i > drawOffsetIndex {
r.LineTo(x, y)
} else {
r.MoveTo(x, y)
}
}
y = cb - yrange.Translate(vy2)
r.LineTo(x, y)
for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
vx, vy2 = xvalues[i], y2values[i]
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy2)
r.LineTo(x, y)
}
r.Close()
r.FillStroke()
}
// HistogramSeries draws a value provider as boxes from 0.
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
if vs.Len() == 0 {
return
}
//calculate bar width?
seriesLength := vs.Len()
barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength)))
if len(barWidths) > 0 {
barWidth = barWidths[0]
}
cb := canvasBox.Bottom
cl := canvasBox.Left
//foreach datapoint, draw a box.
for index := 0; index < seriesLength; index++ {
vx, vy := vs.GetValues(index)
y0 := yrange.Translate(0)
x := cl + xrange.Translate(vx)
y := yrange.Translate(vy)
d.Box(r, Box{
Top: cb - y0,
Left: x - (barWidth >> 1),
Right: x + (barWidth >> 1),
Bottom: cb - y,
}, style)
}
}
// MeasureAnnotation measures how big an annotation would be.
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
style.WriteToRenderer(r)
defer r.ResetStyle()
textBox := r.MeasureText(label)
textWidth := textBox.Width()
textHeight := textBox.Height()
halfTextHeight := textHeight >> 1
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
strokeWidth := style.GetStrokeWidth()
top := ly - (pt + halfTextHeight)
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
bottom := ly + (pb + halfTextHeight)
return Box{
Top: top,
Left: lx,
Right: right,
Bottom: bottom,
}
}
// Annotation draws an anotation with a renderer.
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
style.GetTextOptions().WriteToRenderer(r)
defer r.ResetStyle()
textBox := r.MeasureText(label)
textWidth := textBox.Width()
halfTextHeight := textBox.Height() >> 1
style.GetFillAndStrokeOptions().WriteToRenderer(r)
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
textX := lx + pl + DefaultAnnotationDeltaWidth
textY := ly + halfTextHeight
ltx := lx + DefaultAnnotationDeltaWidth
lty := ly - (pt + halfTextHeight)
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rty := ly - (pt + halfTextHeight)
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rby := ly + (pb + halfTextHeight)
lbx := lx + DefaultAnnotationDeltaWidth
lby := ly + (pb + halfTextHeight)
r.MoveTo(lx, ly)
r.LineTo(ltx, lty)
r.LineTo(rtx, rty)
r.LineTo(rbx, rby)
r.LineTo(lbx, lby)
r.LineTo(lx, ly)
r.Close()
r.FillStroke()
style.GetTextOptions().WriteToRenderer(r)
r.Text(label, textX, textY)
}
// Box draws a box with a given style.
func (d draw) Box(r Renderer, b Box, s Style) {
s.GetFillAndStrokeOptions().WriteToRenderer(r)
defer r.ResetStyle()
r.MoveTo(b.Left, b.Top)
r.LineTo(b.Right, b.Top)
r.LineTo(b.Right, b.Bottom)
r.LineTo(b.Left, b.Bottom)
r.LineTo(b.Left, b.Top)
r.FillStroke()
}
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
}
func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
s.GetFillAndStrokeOptions().WriteToRenderer(r)
defer r.ResetStyle()
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
r.Close()
r.FillStroke()
}
// DrawText draws text with a given style.
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
style.GetTextOptions().WriteToRenderer(r)
defer r.ResetStyle()
r.Text(text, x, y)
}
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
style.GetTextOptions().WriteToRenderer(r)
defer r.ResetStyle()
return r.MeasureText(text)
}
// TextWithin draws the text within a given box.
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
style.GetTextOptions().WriteToRenderer(r)
defer r.ResetStyle()
lines := Text.WrapFit(r, text, box.Width(), style)
linesBox := Text.MeasureLines(r, lines, style)
y := box.Top
switch style.GetTextVerticalAlign() {
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
y = y - linesBox.Height()
case TextVerticalAlignMiddle:
y = y + (box.Height() >> 1) - (linesBox.Height() >> 1)
case TextVerticalAlignMiddleBaseline:
y = y + (box.Height() >> 1) - linesBox.Height()
}
var tx, ty int
for _, line := range lines {
lineBox := r.MeasureText(line)
switch style.GetTextHorizontalAlign() {
case TextHorizontalAlignCenter:
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
case TextHorizontalAlignRight:
tx = box.Right - lineBox.Width()
default:
tx = box.Left
}
if style.TextRotationDegrees == 0 {
ty = y + lineBox.Height()
} else {
ty = y
}
r.Text(line, tx, ty)
y += lineBox.Height() + style.GetTextLineSpacing()
}
}

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,12 +154,65 @@ 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.
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
fa := float64(a) / 255.0
var c Color
c.R = uint8(float64(r) / fa)
c.G = uint8(float64(g) / fa)
c.B = uint8(float64(b) / fa)
c.A = uint8(a | (a >> 8))
return c
}
// ColorChannelFromFloat returns a normalized byte from a given float value.
func ColorChannelFromFloat(v float64) uint8 {
return uint8(v * 255)
}
// Color is our internal color type because color.Color is bullshit. // Color is our internal color type because color.Color is bullshit.
type Color struct { type Color struct {
R uint8 R, G, B, A uint8
G uint8
B uint8
A uint8
} }
// RGBA returns the color as a pre-alpha mixed color set. // RGBA returns the color as a pre-alpha mixed color set.
@ -88,6 +249,24 @@ func (c Color) WithAlpha(a uint8) Color {
} }
} }
// Equals returns true if the color equals another.
func (c Color) Equals(other Color) bool {
return c.R == other.R &&
c.G == other.G &&
c.B == other.B &&
c.A == other.A
}
// AverageWith averages two colors.
func (c Color) AverageWith(other Color) Color {
return Color{
R: (c.R + other.R) >> 1,
G: (c.G + other.G) >> 1,
B: (c.B + other.B) >> 1,
A: c.A,
}
}
// String returns a css string representation of the color. // String returns a css string representation of the color.
func (c Color) String() string { func (c Color) String() string {
fa := float64(c.A) / float64(255) fa := float64(c.A) / float64(255)

View file

@ -1,41 +1,114 @@
package drawing package drawing
import ( import (
"fmt"
"testing" "testing"
"github.com/blendlabs/go-assert" "image/color"
"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) {
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
testutil.AssertTrue(t, black.Equals(ColorBlack), black.String())
white := ColorFromAlphaMixedRGBA(color.White.RGBA())
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

@ -1,8 +1,6 @@
package drawing package drawing
import ( import "math"
"math"
)
const ( const (
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines // CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
@ -98,31 +96,60 @@ func SubdivideQuad(c, c1, c2 []float64) {
return return
} }
func traceWindowIndices(i int) (startAt, endAt int) {
startAt = i * 6
endAt = startAt + 6
return
}
func traceCalcDeltas(c []float64) (dx, dy, d float64) {
dx = c[4] - c[0]
dy = c[5] - c[1]
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
return
}
func traceIsFlat(dx, dy, d, threshold float64) bool {
return (d * d) < threshold*(dx*dx+dy*dy)
}
func traceGetWindow(curves []float64, i int) []float64 {
startAt, endAt := traceWindowIndices(i)
return curves[startAt:endAt]
}
// TraceQuad generate lines subdividing the curve using a Liner // TraceQuad generate lines subdividing the curve using a Liner
// flattening_threshold helps determines the flattening expectation of the curve // flattening_threshold helps determines the flattening expectation of the curve
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) { func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
const curveLen = CurveRecursionLimit * 6
const curveEndIndex = curveLen - 1
const lastIteration = CurveRecursionLimit - 1
// Allocates curves stack // Allocates curves stack
var curves [CurveRecursionLimit * 6]float64 curves := make([]float64, curveLen)
// copy 6 elements from the quad path to the stack
copy(curves[0:6], quad[0:6]) copy(curves[0:6], quad[0:6])
i := 0
// current curve var i int
var c []float64 var c []float64
var dx, dy, d float64 var dx, dy, d float64
for i >= 0 { for i >= 0 {
c = curves[i*6:] c = traceGetWindow(curves, i)
dx = c[4] - c[0] dx, dy, d = traceCalcDeltas(c)
dy = c[5] - c[1]
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx)) // bail early if the distance is 0
if d == 0 {
return
}
// if it's flat then trace a line // if it's flat then trace a line
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 { if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
t.LineTo(c[4], c[5]) t.LineTo(c[4], c[5])
i-- i--
} else { } else {
// second half of bezier go lower onto the stack SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
i++ i++
} }
} }

35
drawing/curve_test.go Normal file
View file

@ -0,0 +1,35 @@
package drawing
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
type point struct {
X, Y float64
}
type mockLine struct {
inner []point
}
func (ml *mockLine) LineTo(x, y float64) {
ml.inner = append(ml.inner, point{x, y})
}
func (ml mockLine) Len() int {
return len(ml.inner)
}
func TestTraceQuad(t *testing.T) {
// replaced new assertions helper
// Quad
// x1, y1, cpx1, cpy2, x2, y2 float64
// do the 9->12 circle segment
quad := []float64{10, 20, 20, 20, 20, 10}
liner := &mockLine{}
TraceQuad(liner, quad, 0.5)
testutil.AssertNotZero(t, liner.Len())
}

View file

@ -23,10 +23,10 @@ type Flattener interface {
// Flatten convert curves into straight segments keeping join segments info // Flatten convert curves into straight segments keeping join segments info
func Flatten(path *Path, flattener Flattener, scale float64) { func Flatten(path *Path, flattener Flattener, scale float64) {
// First Point // First Point
var startX, startY float64 = 0, 0 var startX, startY float64
// Current Point // Current Point
var x, y float64 = 0, 0 var x, y float64
i := 0 var i int
for _, cmp := range path.Components { for _, cmp := range path.Components {
switch cmp { switch cmp {
case MoveToComponent: case MoveToComponent:
@ -43,6 +43,7 @@ func Flatten(path *Path, flattener Flattener, scale float64) {
flattener.LineJoin() flattener.LineJoin()
i += 2 i += 2
case QuadCurveToComponent: case QuadCurveToComponent:
// we include the previous point for the start of the curve
TraceQuad(flattener, path.Points[i-2:], 0.5) TraceQuad(flattener, path.Points[i-2:], 0.5)
x, y = path.Points[i+2], path.Points[i+3] x, y = path.Points[i+2], path.Points[i+3]
flattener.LineTo(x, y) flattener.LineTo(x, y)
@ -64,27 +65,33 @@ func Flatten(path *Path, flattener Flattener, scale float64) {
flattener.End() flattener.End()
} }
// SegmentedPath is a path of disparate point sectinos.
type SegmentedPath struct { type SegmentedPath struct {
Points []float64 Points []float64
} }
// MoveTo implements the path interface.
func (p *SegmentedPath) MoveTo(x, y float64) { func (p *SegmentedPath) MoveTo(x, y float64) {
p.Points = append(p.Points, x, y) p.Points = append(p.Points, x, y)
// TODO need to mark this point as moveto // TODO need to mark this point as moveto
} }
// LineTo implements the path interface.
func (p *SegmentedPath) LineTo(x, y float64) { func (p *SegmentedPath) LineTo(x, y float64) {
p.Points = append(p.Points, x, y) p.Points = append(p.Points, x, y)
} }
// LineJoin implements the path interface.
func (p *SegmentedPath) LineJoin() { func (p *SegmentedPath) LineJoin() {
// TODO need to mark the current point as linejoin // TODO need to mark the current point as linejoin
} }
// Close implements the path interface.
func (p *SegmentedPath) Close() { func (p *SegmentedPath) Close() {
// TODO Close // TODO Close
} }
// End implements the path interface.
func (p *SegmentedPath) End() { func (p *SegmentedPath) End() {
// Nothing to do // Nothing to do
} }

View file

@ -40,7 +40,7 @@ func minMax(x, y float64) (min, max float64) {
return x, y return x, y
} }
// Transform applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle // TransformRectangle applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle
func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) { func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) {
points := []float64{x0, y0, x2, y0, x2, y2, x0, y2} points := []float64{x0, y0, x2, y0, x2, y2, x0, y2}
tr.Transform(points) tr.Transform(points)
@ -129,6 +129,7 @@ func (tr *Matrix) Inverse() {
tr[5] = (tr1*tr4 - tr0*tr5) / d tr[5] = (tr1*tr4 - tr0*tr5) / d
} }
// Copy copies the matrix.
func (tr Matrix) Copy() Matrix { func (tr Matrix) Copy() Matrix {
var result Matrix var result Matrix
copy(result[:], tr[:]) copy(result[:], tr[:])
@ -160,10 +161,10 @@ func (tr *Matrix) Translate(tx, ty float64) {
tr[5] = ty*tr[3] + tx*tr[1] + tr[5] tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
} }
// Rotate adds a rotation to the matrix. angle is in radian // Rotate adds a rotation to the matrix.
func (tr *Matrix) Rotate(angle float64) { func (tr *Matrix) Rotate(radians float64) {
c := math.Cos(angle) c := math.Cos(radians)
s := math.Sin(angle) s := math.Sin(radians)
t0 := c*tr[0] + s*tr[2] t0 := c*tr[0] + s*tr[2]
t1 := s*tr[3] + c*tr[1] t1 := s*tr[3] + c*tr[1]
t2 := c*tr[2] - s*tr[0] t2 := c*tr[2] - s*tr[0]
@ -174,12 +175,12 @@ func (tr *Matrix) Rotate(angle float64) {
tr[3] = t3 tr[3] = t3
} }
// GetTranslation // GetTranslation gets the matrix traslation.
func (tr Matrix) GetTranslation() (x, y float64) { func (tr Matrix) GetTranslation() (x, y float64) {
return tr[4], tr[5] return tr[4], tr[5]
} }
// GetScaling // GetScaling gets the matrix scaling.
func (tr Matrix) GetScaling() (x, y float64) { func (tr Matrix) GetScaling() (x, y float64) {
return tr[0], tr[3] return tr[0], tr[3]
} }
@ -194,9 +195,9 @@ func (tr Matrix) GetScale() float64 {
// ******************** Testing ******************** // ******************** Testing ********************
// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements. // Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements.
func (tr1 Matrix) Equals(tr2 Matrix) bool { func (tr Matrix) Equals(tr2 Matrix) bool {
for i := 0; i < 6; i = i + 1 { for i := 0; i < 6; i = i + 1 {
if !fequals(tr1[i], tr2[i]) { if !fequals(tr[i], tr2[i]) {
return false return false
} }
} }

View file

@ -101,10 +101,10 @@ func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
} }
// ArcTo adds an arc to the path // ArcTo adds an arc to the path
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
endAngle := startAngle + angle endAngle := startAngle + delta
clockWise := true clockWise := true
if angle < 0 { if delta < 0 {
clockWise = false clockWise = false
} }
// normalize // normalize
@ -124,7 +124,7 @@ func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
} else { } else {
p.MoveTo(startX, startY) p.MoveTo(startX, startY)
} }
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, angle) p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, delta)
p.x = cx + math.Cos(endAngle)*rx p.x = cx + math.Cos(endAngle)*rx
p.y = cy + math.Sin(endAngle)*ry p.y = cy + math.Sin(endAngle)*ry
} }

View file

@ -171,8 +171,8 @@ func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
} }
// ArcTo draws an arc. // ArcTo draws an arc.
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, angle) gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, delta)
} }
// Close closes a path. // Close closes a path.

View file

@ -1,6 +1,3 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 13/12/2010 by Laurent Le Goff
package drawing package drawing
import ( import (
@ -25,14 +22,10 @@ func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
} else { } else {
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
} }
} else { } else if !on0 {
if on0 { midX := (q0X + qX) / 2
// No-op. midY := (q0Y + qY) / 2
} else { path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
midX := (q0X + qX) / 2
midY := (q0Y + qY) / 2
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
}
} }
q0X, q0Y, on0 = qX, qY, on q0X, q0Y, on0 = qX, qY, on
} }

View file

@ -1,253 +0,0 @@
package chart
import "github.com/wcharczuk/go-chart/drawing"
// DrawLineSeries draws a line series with a renderer.
func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
if vs.Len() == 0 {
return
}
cb := canvasBox.Bottom
cl := canvasBox.Left
v0x, v0y := vs.GetValue(0)
x0 := cl + xrange.Translate(v0x)
y0 := cb - yrange.Translate(v0y)
var vx, vy float64
var x, y int
fill := s.GetFillColor()
if !fill.IsZero() {
r.SetFillColor(fill)
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValue(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
}
r.LineTo(x, cb)
r.LineTo(x0, cb)
r.Close()
r.Fill()
}
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValue(i)
x = cl + xrange.Translate(vx)
y = cb - yrange.Translate(vy)
r.LineTo(x, y)
}
r.Stroke()
}
// MeasureAnnotation measures how big an annotation would be.
func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box {
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label)
textWidth := textBox.Width()
textHeight := textBox.Height()
halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
strokeWidth := s.GetStrokeWidth()
top := ly - (pt + halfTextHeight)
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
bottom := ly + (pb + halfTextHeight)
return Box{
Top: top,
Left: lx,
Right: right,
Bottom: bottom,
}
}
// DrawAnnotation draws an anotation with a renderer.
func DrawAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) {
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray())
textBox := r.MeasureText(label)
textWidth := textBox.Width()
halfTextHeight := textBox.Height() >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
textX := lx + pl + DefaultAnnotationDeltaWidth
textY := ly + halfTextHeight
ltx := lx + DefaultAnnotationDeltaWidth
lty := ly - (pt + halfTextHeight)
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rty := ly - (pt + halfTextHeight)
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rby := ly + (pb + halfTextHeight)
lbx := lx + DefaultAnnotationDeltaWidth
lby := ly + (pb + halfTextHeight)
r.MoveTo(lx, ly)
r.LineTo(ltx, lty)
r.LineTo(rtx, rty)
r.LineTo(rbx, rby)
r.LineTo(lbx, lby)
r.LineTo(lx, ly)
r.Close()
r.FillStroke()
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
r.Text(label, textX, textY)
}
// DrawBox draws a box with a given style.
func DrawBox(r Renderer, b Box, s Style) {
r.SetFillColor(s.GetFillColor())
r.SetStrokeColor(s.GetStrokeColor(DefaultStrokeColor))
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.MoveTo(b.Left, b.Top)
r.LineTo(b.Right, b.Top)
r.LineTo(b.Right, b.Bottom)
r.LineTo(b.Left, b.Bottom)
r.LineTo(b.Left, b.Top)
r.FillStroke()
}
// DrawText draws text with a given style.
func DrawText(r Renderer, text string, x, y int, s Style) {
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
r.Text(text, x, y)
}
// DrawTextCentered draws text with a given style centered.
func DrawTextCentered(r Renderer, text string, x, y int, s Style) {
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
tb := r.MeasureText(text)
tx := x - (tb.Width() >> 1)
ty := y - (tb.Height() >> 1)
r.Text(text, tx, ty)
}
// CreateLegend returns a legend renderable function.
func CreateLegend(c *Chart, style Style) Renderable {
return func(r Renderer, cb Box, defaults Style) {
workingStyle := style.WithDefaultsFrom(defaults.WithDefaultsFrom(Style{
FillColor: drawing.ColorWhite,
FontColor: DefaultTextColor,
FontSize: 8.0,
StrokeColor: DefaultAxisColor,
StrokeWidth: DefaultAxisLineWidth,
}))
// DEFAULTS
legendPadding := 5
lineTextGap := 5
lineLengthMinimum := 25
var labels []string
var lines []Style
for _, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show {
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
labels = append(labels, s.GetName())
lines = append(lines, s.GetStyle())
}
}
}
legend := Box{
Top: cb.Top, //padding
Left: cb.Left,
}
legendContent := Box{
Top: legend.Top + legendPadding,
Left: legend.Left + legendPadding,
}
r.SetFontColor(workingStyle.GetFontColor())
r.SetFontSize(workingStyle.GetFontSize())
// measure
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
tb := r.MeasureText(labels[x])
legendContent.Bottom += (tb.Height() + DefaultMinimumTickVerticalSpacing)
rowRight := tb.Width() + legendContent.Left + lineLengthMinimum + lineTextGap
legendContent.Right = MaxInt(legendContent.Right, rowRight)
}
}
legend = legend.Grow(legendContent)
DrawBox(r, legend, workingStyle)
legendContent.Right = legend.Right - legendPadding
legendContent.Bottom = legend.Bottom - legendPadding
ycursor := legendContent.Top
tx := legendContent.Left
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
tb := r.MeasureText(labels[x])
ycursor += tb.Height()
r.Text(labels[x], tx, ycursor)
th2 := tb.Height() >> 1
lx := tx + tb.Width() + lineTextGap
ly := ycursor - th2
lx2 := legendContent.Right - legendPadding
r.SetStrokeColor(lines[x].GetStrokeColor())
r.SetStrokeWidth(lines[x].GetStrokeWidth())
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
r.MoveTo(lx, ly)
r.LineTo(lx2, ly)
r.Stroke()
ycursor += DefaultMinimumTickVerticalSpacing
}
}
}
}

131
ema_series.go Normal file
View file

@ -0,0 +1,131 @@
package chart
import "fmt"
const (
// DefaultEMAPeriod is the default EMA period used in the sigma calculation.
DefaultEMAPeriod = 12
)
// Interface Assertions.
var (
_ Series = (*EMASeries)(nil)
_ FirstValuesProvider = (*EMASeries)(nil)
_ LastValuesProvider = (*EMASeries)(nil)
)
// EMASeries is a computed series.
type EMASeries struct {
Name string
Style Style
YAxis YAxisType
Period int
InnerSeries ValuesProvider
cache []float64
}
// GetName returns the name of the time series.
func (ema EMASeries) GetName() string {
return ema.Name
}
// GetStyle returns the line style.
func (ema EMASeries) GetStyle() Style {
return ema.Style
}
// GetYAxis returns which YAxis the series draws on.
func (ema EMASeries) GetYAxis() YAxisType {
return ema.YAxis
}
// GetPeriod returns the window size.
func (ema EMASeries) GetPeriod() int {
if ema.Period == 0 {
return DefaultEMAPeriod
}
return ema.Period
}
// Len returns the number of elements in the series.
func (ema EMASeries) Len() int {
return ema.InnerSeries.Len()
}
// GetSigma returns the smoothing factor for the serise.
func (ema EMASeries) GetSigma() float64 {
return 2.0 / (float64(ema.GetPeriod()) + 1)
}
// GetValues gets a value at a given index.
func (ema *EMASeries) GetValues(index int) (x, y float64) {
if ema.InnerSeries == nil {
return
}
if len(ema.cache) == 0 {
ema.ensureCachedValues()
}
vx, _ := ema.InnerSeries.GetValues(index)
x = vx
y = ema.cache[index]
return
}
// GetFirstValues computes the first moving average value.
func (ema *EMASeries) GetFirstValues() (x, y float64) {
if ema.InnerSeries == nil {
return
}
if len(ema.cache) == 0 {
ema.ensureCachedValues()
}
x, _ = ema.InnerSeries.GetValues(0)
y = ema.cache[0]
return
}
// GetLastValues computes the last moving average value but walking back window size samples,
// and recomputing the last moving average chunk.
func (ema *EMASeries) GetLastValues() (x, y float64) {
if ema.InnerSeries == nil {
return
}
if len(ema.cache) == 0 {
ema.ensureCachedValues()
}
lastIndex := ema.InnerSeries.Len() - 1
x, _ = ema.InnerSeries.GetValues(lastIndex)
y = ema.cache[lastIndex]
return
}
func (ema *EMASeries) ensureCachedValues() {
seriesLength := ema.InnerSeries.Len()
ema.cache = make([]float64, seriesLength)
sigma := ema.GetSigma()
for x := 0; x < seriesLength; x++ {
_, y := ema.InnerSeries.GetValues(x)
if x == 0 {
ema.cache[x] = y
continue
}
previousEMA := ema.cache[x-1]
ema.cache[x] = ((y - previousEMA) * sigma) + previousEMA
}
}
// Render renders the series.
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := ema.Style.InheritFrom(defaults)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
}
// Validate validates the series.
func (ema *EMASeries) Validate() error {
if ema.InnerSeries == nil {
return fmt.Errorf("ema series requires InnerSeries to be set")
}
return nil
}

105
ema_series_test.go Normal file
View file

@ -0,0 +1,105 @@
package chart
import (
"testing"
"git.smarteching.com/zeni/go-chart/v2/testutil"
)
var (
emaXValues = LinearRange(1.0, 50.0)
emaYValues = []float64{
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,
1, 2,
}
emaExpected = []float64{
1,
1.074074074,
1.216735254,
1.422903013,
1.68787316,
1.859141815,
1.943649828,
1.947823915,
1.877614736,
1.886680311,
1.969148437,
2.119581886,
2.33294619,
2.456431658,
2.496695979,
2.459903685,
2.351762671,
2.325706177,
2.375653867,
2.495975803,
2.681459077,
2.779128775,
2.795489607,
2.73656445,
2.607930047,
2.562898191,
2.595276103,
2.699329725,
2.869749746,
2.953471987,
2.956918506,
2.886035654,
2.746329309,
2.691045657,
2.713931163,
2.809195522,
2.971477335,
3.047664199,
3.044133518,
2.966790294,
2.821102124,
2.760279745,
2.778036801,
2.868552593,
3.026437586,
3.098553321,
3.091253075,
3.010419514,
2.86149955,
2.797684768,
}
emaDelta = 0.0001
)
func TestEMASeries(t *testing.T) {
// replaced new assertions helper
mockSeries := mockValuesProvider{
emaXValues,
emaYValues,
}
testutil.AssertEqual(t, 50, mockSeries.Len())
ema := &EMASeries{
InnerSeries: mockSeries,
Period: 26,
}
sig := ema.GetSigma()
testutil.AssertEqual(t, 2.0/(26.0+1), sig)
var yvalues []float64
for x := 0; x < ema.Len(); x++ {
_, y := ema.GetValues(x)
yvalues = append(yvalues, y)
}
for index, yv := range yvalues {
testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta)
}
lvx, lvy := ema.GetLastValues()
testutil.AssertEqual(t, 50.0, lvx)
testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta)
}

View file

@ -0,0 +1,44 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
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).
It is important to not that the chart automatically sizes the canvas box to fit the annotations,
As well as automatically assign a series color for the `Stroke` or border component of the series.
The annotation series is most often used by the original author to show the last value of another series, but
they can be used in other capacities as well.
*/
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},
},
chart.AnnotationSeries{
Annotations: []chart.Value2{
{XValue: 1.0, YValue: 1.0, Label: "One"},
{XValue: 2.0, YValue: 2.0, Label: "Two"},
{XValue: 3.0, YValue: 3.0, Label: "Three"},
{XValue: 4.0, YValue: 4.0, Label: "Four"},
{XValue: 5.0, YValue: 5.0, Label: "Five"},
},
},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

34
examples/axes/main.go Normal file
View file

@ -0,0 +1,34 @@
package main
//go:generate go run main.go
import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
*/
graph := chart.Chart{
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
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/axes/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,40 @@
package main
//go:generate go run main.go
import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
*/
graph := chart.Chart{
XAxis: chart.XAxis{
Name: "The XAxis",
},
YAxis: chart.YAxis{
Name: "The YAxis",
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
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)
}

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

@ -0,0 +1,55 @@
package main
//go:generate go run main.go
import (
"fmt"
"log"
"net/http"
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func drawChart(res http.ResponseWriter, req *http.Request) {
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: "!!"},
},
}
res.Header().Set("Content-Type", "image/png")
err := graph.Render(chart.PNG, res)
if err != nil {
fmt.Printf("Error rendering chart: %v\n", err)
}
}
func port() string {
if len(os.Getenv("PORT")) > 0 {
return os.Getenv("PORT")
}
return "8080"
}
func main() {
listenPort := fmt.Sprintf(":%s", port())
fmt.Printf("Listening on %s\n", listenPort)
http.HandleFunc("/", drawChart)
log.Fatal(http.ListenAndServe(listenPort, nil))
}

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

@ -0,0 +1,38 @@
package main
//go:generate go run main.go
import (
"fmt"
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
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.
You can also do this for the x-axis, or the secondary y-axis.
This example also shows what the chart looks like with the x-axis left off or not shown.
*/
graph := chart.Chart{
YAxis: chart.YAxis{
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%0.6f", vf)
}
return ""
},
},
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)
}

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

@ -0,0 +1,34 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
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.
*/
graph := chart.Chart{
YAxis: chart.YAxis{
Range: &chart.ContinuousRange{
Min: 0.0,
Max: 10.0,
},
},
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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,38 @@
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() {
/*
In this example we set some custom colors for the series and the chart background and canvas.
*/
graph := chart.Chart{
Background: chart.Style{
FillColor: drawing.ColorBlue,
},
Canvas: chart.Style{
FillColor: drawing.ColorFromHex("efefef"),
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
StrokeColor: drawing.ColorRed, // will supercede defaults
FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults
},
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)
}

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

@ -0,0 +1,42 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
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.
Custom ticks will supercede a custom range, which will supercede automatic generation based on series values.
*/
graph := chart.Chart{
YAxis: chart.YAxis{
Range: &chart.ContinuousRange{
Min: 0.0,
Max: 4.0,
},
Ticks: []chart.Tick{
{Value: 0.0, Label: "0.00"},
{Value: 2.0, Label: "2.00"},
{Value: 4.0, Label: "4.00"},
{Value: 6.0, Label: "6.00"},
{Value: 8.0, Label: "Eight"},
{Value: 10.0, Label: "Ten"},
},
},
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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,49 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
The below will draw the same chart as the `basic` example, except with both the x and y axes turned on.
In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically,
the canvas "box" is adjusted to fit the space the axes occupy so as not to clip.
Additionally, it shows how you can use the "Descending" property of continuous ranges to change the ordering of
how values (including ticks) are drawn.
*/
graph := chart.Chart{
Height: 500,
Width: 500,
XAxis: chart.XAxis{
/*Range: &chart.ContinuousRange{
Descending: true,
},*/
},
YAxis: chart.YAxis{
Range: &chart.ContinuousRange{
Descending: true,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Style: chart.Style{
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
},
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)
}

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

@ -0,0 +1,27 @@
package main
import (
"fmt"
"log"
"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},
},
},
}
collector := &chart.ImageWriter{}
graph.Render(chart.PNG, collector)
image, err := collector.Image()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Final Image: %dx%d\n", image.Bounds().Size().X, image.Bounds().Size().Y)
}

43
examples/legend/main.go Normal file
View file

@ -0,0 +1,43 @@
package main
//go:generate go run main.go
import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
In this example we add a `Renderable` or a custom component to the `Elements` array.
In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series.
If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend.
*/
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 20,
Left: 20,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Name: "A test series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
},
}
//note we have to do this as a separate step because we need a reference to graph
graph.Elements = []chart.Renderable{
chart.Legend(&graph),
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

BIN
examples/legend/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,103 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
In this example we add a `Renderable` or a custom component to the `Elements` array.
In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series.
If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend.
*/
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 20,
Left: 260,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Name: "A test series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Another test series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Yet Another test series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Even More series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Foo Bar",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Bar Baz",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Moo Bar",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Zoo Bar Baz",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
},
chart.ContinuousSeries{
Name: "Fast and the Furious",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0},
},
chart.ContinuousSeries{
Name: "2 Fast 2 Furious",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0},
},
chart.ContinuousSeries{
Name: "They only get more fast and more furious",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0},
},
},
}
//note we have to do this as a separate step because we need a reference to graph
graph.Elements = []chart.Renderable{
chart.LegendLeft(&graph),
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,40 @@
package main
//go:generate go run main.go
import (
"os"
chart "git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument.
InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
*/
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements.
}
// note we create a LinearRegressionSeries series by assignin the inner series.
// we need to use a reference because `.Render()` needs to modify state within the series.
linRegSeries := &chart.LinearRegressionSeries{
InnerSeries: mainSeries,
} // we can optionally set the `WindowSize` property which alters how the moving average is calculated.
graph := chart.Chart{
Series: []chart.Series{
mainSeries,
linRegSeries,
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -0,0 +1,41 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
/*
In this example we set the primary YAxis to have logarithmic range.
*/
graph := chart.Chart{
Background: chart.Style{
Padding: chart.Box{
Top: 20,
Left: 20,
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Name: "A test series",
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
YValues: []float64{1, 10, 100, 1000, 10000},
},
},
YAxis: chart.YAxis{
Style: chart.Shown(),
NameStyle: chart.Shown(),
Range: &chart.LogarithmicRange{},
},
}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

61
examples/min_max/main.go Normal file
View file

@ -0,0 +1,61 @@
package main
//go:generate go run main.go
import (
"os"
"git.smarteching.com/zeni/go-chart/v2"
)
func main() {
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(),
YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(50).WithMax(150)}.Values(),
}
minSeries := &chart.MinSeries{
Style: chart.Style{
StrokeColor: chart.ColorAlternateGray,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
maxSeries := &chart.MaxSeries{
Style: chart.Style{
StrokeColor: chart.ColorAlternateGray,
StrokeDashArray: []float64{5.0, 5.0},
},
InnerSeries: mainSeries,
}
graph := chart.Chart{
Width: 1920,
Height: 1080,
YAxis: chart.YAxis{
Name: "Random Values",
Range: &chart.ContinuousRange{
Min: 25,
Max: 175,
},
},
XAxis: chart.XAxis{
Name: "Random Other Values",
},
Series: []chart.Series{
mainSeries,
minSeries,
maxSeries,
chart.LastValueAnnotationSeries(minSeries),
chart.LastValueAnnotationSeries(maxSeries),
},
}
graph.Elements = []chart.Renderable{chart.Legend(&graph)}
f, _ := os.Create("output.png")
defer f.Close()
graph.Render(chart.PNG, f)
}

BIN
examples/min_max/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

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