Compare commits

..

76 commits
v2.0.3 ... main

Author SHA1 Message Date
958172a1f1 update URL in examples
Some checks failed
Test / Build (push) Has been cancelled
2025-05-13 21:53:31 -05:00
0eacc8e394 start migration to our packages
Some checks are pending
Test / Build (push) Waiting to run
2025-05-13 21:46:02 -05:00
vicanso
d25a827706 fix: fix label position of pie, #86 2024-08-15 20:37:07 +08:00
vicanso
5842c71b1d refactor: remove unused code 2024-08-01 21:44:52 +08:00
vicanso
e7dc4189d5 feat: support bar margin 2024-06-07 20:35:03 +08:00
vicanso
32e6dd52d0 refactor: export GetRenderer function to get chart renderer 2024-06-05 21:13:03 +08:00
vicanso
9614835723 feat: support rounded rect for horizontal bar chart 2024-05-21 20:26:40 +08:00
vicanso
9b7634c2c2 feat: support rounded rect for bar chart 2024-05-16 20:02:24 +08:00
Tree Xie
5a69c3e5a3
Merge pull request #72 from euerla/main
fix: Label position of the pie chart
2024-03-23 10:33:19 +08:00
Alexander Heidrich
8c6c4e007c fix: Label position of the pie chart 2024-03-22 08:27:06 +01:00
Tree Xie
765febd03a
Merge pull request #71 from euerla/main
fix: Label position of the pie chart
2024-03-09 08:13:02 +08:00
Alexander Heidrich
19a4d783fd fix: Label position of the pie chart 2024-03-08 20:24:13 +01:00
vicanso
06fe1006d5 chore: update test go version 2024-02-11 12:39:39 +08:00
vicanso
f1a231ff4b feat: support split line show option for charts, #69 2024-02-11 12:36:26 +08:00
Tree Xie
c7c0655113
Merge pull request #67 from ssexuejinwei/main
support dash line for line chart
2024-01-02 19:35:16 +08:00
xuejinwei.1112
310800a5f0 support dash line for line chart 2024-01-02 12:32:56 +08:00
vicanso
e09ab2c3c7 Revert "chore: update modules"
This reverts commit c2f709a742.
2023-12-27 20:37:18 +08:00
vicanso
c2f709a742 chore: update modules 2023-12-27 20:34:05 +08:00
vicanso
98af9866a4 refactor: support label show for radar chart, #62 2023-12-27 20:33:12 +08:00
Tree Xie
c302d0ffa4
Merge pull request #65 from vicanso/revert-56-xAxisImprovements
Revert "Improvements to how the X Axis is rendered"
2023-12-27 18:21:05 +08:00
Tree Xie
8bcb584aba
Revert "Improvements to how the X Axis is rendered" 2023-12-27 18:20:55 +08:00
vicanso
0ddb9e4ef1 chore: update modules 2023-05-12 20:31:42 +08:00
Tree Xie
18d8ee51fb
Merge pull request #56 from jentfoo/xAxisImprovements
Improvements to how the X Axis is rendered
2023-05-06 19:41:25 +08:00
Mike Jensen
687baad0af
Unit test fixes
Unit tests updated for new tick positions and in a couple cases additional one X axis sample.
2023-05-05 10:19:01 -06:00
Mike Jensen
a158191faf
Add Unit to XAxis as a publicly visible parameter
In some cases the XAxis may have a single long title.  This can result in very few increments being shown.
In order to be more flexible for those cases this allows the XAxis Tick frequency to be able to be directly controlled.
2023-05-05 09:55:55 -06:00
Mike Jensen
c810369730
Change ticks to avoid values impacting each other
The recently introduced logic has an incorrect understanding of the `unit` parameter.
This would result in too many ticks being outputted, particularly as datasets got larger.
This fixes it by re-calculating the tick count using the `unit` param as originally intended.
2023-05-05 09:44:09 -06:00
Mike Jensen
19173dfd37
painter.go: Optimize isTick function
This reduces the loop frequency to one or two iterations in all cases.
I have been unable to find any single line equation that can produce this same behavior, but one likely exists.
2023-05-04 17:59:11 -06:00
Mike Jensen
e7a49c2c21
Improvements to how the X Axis is rendered
This provides two improvements to how the X Axis is rendered:
* The calculation for where a tick should exist has been improved.  It now will ensure a tick is always at both the start of the axis and the end of the axis.  This makes it clear exactly what data span is captured in the graph.
* The second improvement is how the label on the last tick is written.  It used to often get partially cut off, and with the change to ensure a tick is always at the end this could be seen more easily.  Now the last tick has it's label written to the left so that it can be fully displayed.
2023-05-04 12:52:28 -06:00
vicanso
20e8d4a078 feat: support to set the first axis 2023-02-25 14:04:30 +08:00
vicanso
29a5ece545 chore: update go modules 2023-02-14 20:35:54 +08:00
vicanso
d3f7a773af fix: fix zero value of funnel chart, #43 2023-01-12 20:20:36 +08:00
vicanso
8ba9e2e1b2 fix: fix x axis label of horizontal bar chart, #42 2023-01-11 20:41:16 +08:00
vicanso
e10175594b feat: support label format for funnel chart, #41 2023-01-05 19:15:58 +08:00
Tree Xie
b3cb5a75cb
Merge pull request #40 from junglerider/main
added option for line chart bg fill opacity
2022-12-27 08:29:19 +08:00
Thomas Knierim
a767b3e1af added option for line chart bg fill opacity 2022-12-26 15:06:53 +07:00
vicanso
830d4bdd21 fix: fix test for text roration 2022-12-11 14:59:37 +08:00
vicanso
d5533447f5 feat: support text rotation for series label, #38 2022-12-11 14:57:05 +08:00
vicanso
ef04ac14ab feat: support font size for series label, #38 2022-12-09 20:08:47 +08:00
vicanso
f9a534ea02 fix: fix the color of series label, #37 2022-12-07 19:57:35 +08:00
vicanso
df6180e59a fix: fix zero max value of nan, #37 2022-11-28 19:55:14 +08:00
vicanso
5f0aec60d3 refactor: adjust label value of horizontal bar 2022-11-24 20:12:19 +08:00
vicanso
6db8e2c8dc feat: support series label for horizontal bar 2022-11-23 23:01:52 +08:00
vicanso
4fc250aefc feat: support rotate series label 2022-11-22 22:41:56 +08:00
vicanso
55eca7b0b9 feat: support detect color dark or light 2022-11-16 20:46:19 +08:00
vicanso
a42d0727df feat: support text rotation 2022-11-15 20:09:29 +08:00
vicanso
7e1f003be8 refactor: update demo 2022-11-12 20:18:02 +08:00
vicanso
de4250f60b feat: support get and set default font 2022-11-12 20:01:36 +08:00
vicanso
2ed86a81d0 fix: fix setting font family for table render 2022-11-12 10:48:24 +08:00
vicanso
6f6d6c3447 fix: fix label render of pie chart, #34 2022-11-07 20:34:28 +08:00
vicanso
bdcc871ab1 fix: fix series render of horizontal bar, #31 2022-11-03 21:31:53 +08:00
vicanso
a88e607bfc refactor: support custom value formatter 2022-10-21 20:37:09 +08:00
vicanso
74a47a9858 refactor: enhance value format, #28 2022-10-20 20:27:42 +08:00
vicanso
0a1061a8db docs: update document 2022-10-11 20:17:22 +08:00
vicanso
6652ece0fe feat: support bar height for horizontal bar chart 2022-09-29 20:20:54 +08:00
vicanso
0a80e7056f feat: support setting bar width for bar chart, #24 2022-09-28 20:29:22 +08:00
vicanso
1f5b9d513e refactor: adjust series label render 2022-09-23 20:50:42 +08:00
vicanso
de49ef8c68 feat: support label for line chart, #23 2022-09-22 20:10:45 +08:00
vicanso
825e65d930 refactor: use MaxInt32 instead of MaxInt 2022-09-15 20:15:05 +08:00
vicanso
50605907c7 feat: support null value for line chart 2022-09-15 20:09:00 +08:00
vicanso
bb9af986be chore: update go modules 2022-09-02 20:42:10 +08:00
vicanso
4a1ff80556 fix: fix min and max option of y axis 2022-09-01 20:20:51 +08:00
vicanso
128d5b2774 refactor: adjust max value of axis, #19 2022-08-28 09:43:18 +08:00
vicanso
dc1a89d3ff feat: support fill area of line chart 2022-08-25 20:19:05 +08:00
vicanso
93e03856ca fix: fix NaN of radar chart, #17 2022-08-10 20:39:14 +08:00
vicanso
550b9874d2 refactor: remove unused path 2022-07-29 20:42:13 +08:00
vicanso
e530adccb6 feat: support stroke width of line chart 2022-07-28 20:49:00 +08:00
vicanso
817fceff73 feat: support hide symbol of line chart 2022-07-27 20:32:31 +08:00
vicanso
e095223705 fix: fix font setting for title, #15 2022-07-27 20:27:49 +08:00
vicanso
1713bc283f docs: add doc 2022-07-26 20:45:04 +08:00
vicanso
cac6fd03d3 fix: fix unit count of xasix 2022-07-26 20:44:50 +08:00
vicanso
3d20bea846 refactor: remove unused code 2022-07-22 20:25:12 +08:00
vicanso
8740c55a1a feat: support padding for legend 2022-07-19 20:12:31 +08:00
vicanso
3af0d4d445 fix: fix pie chart legend 2022-07-14 20:14:32 +08:00
vicanso
b5b2d37e87 fix: fix axis boundary gap, #13 2022-07-11 20:44:28 +08:00
vicanso
805f4381a3 fix: fix multi line legend 2022-07-11 20:20:41 +08:00
vicanso
959377542e fix: fix multi line label 2022-07-08 21:11:47 +08:00
58 changed files with 2143 additions and 328 deletions

View file

@ -14,12 +14,12 @@ jobs:
strategy:
matrix:
go:
- '1.22'
- '1.21'
- '1.20'
- '1.19'
- '1.18'
- '1.17'
- '1.16'
- '1.15'
- '1.14'
- '1.13'
steps:
- name: Go ${{ matrix.go }} test

2
.gitignore vendored
View file

@ -16,3 +16,5 @@
*.png
*.svg
tmp
NotoSansSC.ttf
.vscode

View file

@ -1,5 +1,7 @@
# go-charts
Clone from https://github.com/vicanso/go-charts
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions)
@ -33,7 +35,7 @@ More examples can be found in the [./examples/](./examples/) directory.
package main
import (
charts "github.com/vicanso/go-charts/v2"
charts "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -99,7 +101,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -174,7 +176,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -231,7 +233,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -286,7 +288,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -344,7 +346,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -384,7 +386,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -449,7 +451,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {

View file

@ -32,7 +32,7 @@
package main
import (
charts "github.com/vicanso/go-charts/v2"
charts "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -98,7 +98,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -173,7 +173,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -230,7 +230,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -285,7 +285,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -343,7 +343,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -383,7 +383,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -447,7 +447,7 @@ func main() {
package main
import (
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@ -569,7 +569,7 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op
默认使用的字符为`roboto`为英文字体库,因此如果需要显示中文字符需要增加中文字体库,`InstallFont`函数可添加对应的字体库,成功添加之后则指定`title.textStyle.fontFamily`即可。
在浏览器中使用`svg`时,如果指定的`fontFamily`不支持中文字符,展示的中文并不会乱码,但是会导致在计算字符宽度等错误。
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败字体尽量选择Bold类型否则生成的图片会有点模糊
示例见 [examples/chinese/main.go](examples/chinese/main.go)

View file

@ -23,8 +23,8 @@
package charts
import (
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
type Box = chart.Box

53
axis.go
View file

@ -26,7 +26,7 @@ import (
"strings"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type axisPainter struct {
@ -63,6 +63,8 @@ type AxisOption struct {
StrokeWidth float64
// The length of the axis tick
TickLength int
// The first axis
FirstAxis int
// The margin value of label
LabelMargin int
// The font size of label
@ -75,6 +77,11 @@ type AxisOption struct {
SplitLineShow bool
// The color of split line
SplitLineColor Color
// The text rotation of label
TextRotation float64
// The offset of label
LabelOffset Box
Unit int
}
func (a *axisPainter) Render() (Box, error) {
@ -152,15 +159,30 @@ func (a *axisPainter) Render() (Box, error) {
}
top.SetDrawingStyle(style).OverrideTextStyle(style)
isTextRotation := opt.TextRotation != 0
if isTextRotation {
top.SetTextRotation(opt.TextRotation)
}
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
if isTextRotation {
top.ClearTextRotation()
}
// 增加30px来计算文本展示区域
textFillWidth := float64(textMaxWidth + 20)
textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth)
unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber)))
// 偶数
if unit%2 == 0 && dataCount%(unit+1) == 0 {
unit++
// 根据文本宽度计算较为符合的展示项
fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth)
unit := opt.Unit
if unit <= 0 {
unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount))
unit = chart.MaxInt(unit, opt.SplitNumber)
// 偶数
if unit%2 == 0 && dataCount%(unit+1) == 0 {
unit++
}
}
width := 0
@ -235,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) {
Length: tickLength,
Unit: unit,
Orient: orient,
First: opt.FirstAxis,
})
p.LineStroke([]Point{
{
@ -253,15 +276,19 @@ func (a *axisPainter) Render() (Box, error) {
Top: labelPaddingTop,
Right: labelPaddingRight,
})).MultiText(MultiTextOption{
Align: textAlign,
TextList: data,
Orient: orient,
Unit: unit,
Position: labelPosition,
First: opt.FirstAxis,
Align: textAlign,
TextList: data,
Orient: orient,
Unit: unit,
Position: labelPosition,
TextRotation: opt.TextRotation,
Offset: opt.LabelOffset,
})
// 显示辅助线
if opt.SplitLineShow {
style.StrokeColor = opt.SplitLineColor
style.StrokeWidth = 1
top.OverrideDrawingStyle(style)
if isVertical {
x0 := p.Width()
@ -270,7 +297,9 @@ func (a *axisPainter) Render() (Box, error) {
x0 = 0
x1 = top.Width() - p.Width()
}
for _, y := range autoDivide(height, tickCount) {
yValues := autoDivide(height, tickCount)
yValues = yValues[0 : len(yValues)-1]
for _, y := range yValues {
top.LineStroke([]Point{
{
X: x0,

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestAxis(t *testing.T) {
@ -113,7 +113,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 36 0\nL 41 0\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 66\nL 41 66\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 133\nL 41 133\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 200\nL 41 200\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 266\nL 41 266\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 333\nL 41 333\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 400\nL 41 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 41 0\nL 41 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"0\" y=\"7\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"4\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"0\" y=\"140\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"4\" y=\"207\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"13\" y=\"273\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"8\" y=\"340\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"4\" y=\"407\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sun</text><path d=\"M 41 0\nL 600 0\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 66\nL 600 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 133\nL 600 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 200\nL 600 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 266\nL 600 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 333\nL 600 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 400\nL 600 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 36 0\nL 41 0\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 66\nL 41 66\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 133\nL 41 133\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 200\nL 41 200\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 266\nL 41 266\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 333\nL 41 333\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 36 400\nL 41 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 41 0\nL 41 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"0\" y=\"7\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"4\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"0\" y=\"140\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"4\" y=\"207\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"13\" y=\"273\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"8\" y=\"340\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"4\" y=\"407\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sun</text><path d=\"M 41 0\nL 600 0\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 66\nL 600 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 133\nL 600 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 200\nL 600 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 266\nL 600 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 41 333\nL 600 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
},
// 右侧
{
@ -135,7 +135,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 559 0\nL 564 0\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 66\nL 564 66\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 133\nL 564 133\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 200\nL 564 200\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 266\nL 564 266\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 333\nL 564 333\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 400\nL 564 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 0\nL 559 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"569\" y=\"7\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"569\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"569\" y=\"140\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"569\" y=\"207\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"569\" y=\"273\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"569\" y=\"340\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"569\" y=\"407\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sun</text><path d=\"M 0 0\nL 559 0\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 66\nL 559 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 133\nL 559 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 200\nL 559 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 266\nL 559 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 333\nL 559 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 400\nL 559 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 559 0\nL 564 0\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 66\nL 564 66\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 133\nL 564 133\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 200\nL 564 200\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 266\nL 564 266\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 333\nL 564 333\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 400\nL 564 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 559 0\nL 559 400\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"569\" y=\"7\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"569\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"569\" y=\"140\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"569\" y=\"207\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"569\" y=\"273\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"569\" y=\"340\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"569\" y=\"407\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sun</text><path d=\"M 0 0\nL 559 0\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 66\nL 559 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 133\nL 559 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 200\nL 559 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 266\nL 559 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 333\nL 559 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
},
// 顶部
{

View file

@ -23,8 +23,10 @@
package charts
import (
"math"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type barChart struct {
@ -59,14 +61,10 @@ type BarChartOption struct {
// The option of title
Title TitleOption
// The legend option
Legend LegendOption
}
type barChartLabelRenderOption struct {
Text string
Style Style
X int
Y int
Legend LegendOption
BarWidth int
// Margin of bar
BarMargin int
}
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@ -75,6 +73,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
seriesPainter := result.seriesPainter
xRange := NewRange(AxisRangeOption{
Painter: b.p,
DivideCount: len(opt.XAxis.Data),
Size: seriesPainter.Width(),
})
@ -91,9 +90,17 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
margin = 5
barMargin = 3
}
if opt.BarMargin > 0 {
barMargin = opt.BarMargin
}
seriesCount := len(seriesList)
// 总的宽度-两个margin-(总数-1)的barMargin
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount
if opt.BarWidth > 0 && opt.BarWidth < barWidth {
barWidth = opt.BarWidth
// 重新计算margin
margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2
}
barMaxHeight := seriesPainter.Height()
theme := opt.Theme
seriesNames := seriesList.Names()
@ -104,7 +111,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
markPointPainter,
markLinePainter,
}
labelRenderOptions := make([]barChartLabelRenderOption, 0)
for index := range seriesList {
series := seriesList[index]
yRange := result.axisRanges[series.AxisIndex]
@ -112,6 +118,18 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
divideValues := xRange.AutoDivide()
points := make([]Point, len(series.Data))
var labelPainter *SeriesLabelPainter
if series.Label.Show {
labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
P: seriesPainter,
SeriesNames: seriesNames,
Label: series.Label,
Theme: opt.Theme,
Font: opt.Font,
})
rendererList = append(rendererList, labelPainter)
}
for j, item := range series.Data {
if j >= xRange.divideCount {
continue
@ -129,14 +147,25 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
}
top := barMaxHeight - h
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
})
if series.RoundRadius <= 0 {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
})
} else {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).RoundedRect(chart.Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
}, series.RoundRadius)
}
// 用于生成marker point
points[j] = Point{
// 居中的位置
@ -150,30 +179,33 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Y: top,
}
// 如果label不需要展示则返回
if !series.Label.Show {
if labelPainter == nil {
continue
}
distance := series.Label.Distance
if distance == 0 {
distance = 5
y := barMaxHeight - h
radians := float64(0)
fontColor := series.Label.Color
if series.Label.Position == PositionBottom {
y = barMaxHeight
radians = -math.Pi / 2
if fontColor.IsZero() {
if isLightColor(fillColor) {
fontColor = defaultLightFontColor
} else {
fontColor = defaultDarkFontColor
}
}
}
text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1)
labelStyle := Style{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
}
if !series.Label.Color.IsZero() {
labelStyle.FontColor = series.Label.Color
}
textBox := seriesPainter.MeasureText(text)
labelRenderOptions = append(labelRenderOptions, barChartLabelRenderOption{
Text: text,
Style: labelStyle,
X: x + (barWidth-textBox.Width())>>1,
Y: barMaxHeight - h - distance,
labelPainter.Add(LabelValue{
Index: index,
Value: item.Value,
X: x + barWidth>>1,
Y: y,
// 旋转
Radians: radians,
FontColor: fontColor,
Offset: series.Label.Offset,
FontSize: series.Label.FontSize,
})
}
@ -192,10 +224,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Range: yRange,
})
}
for _, labelOpt := range labelRenderOptions {
seriesPainter.OverrideTextStyle(labelOpt.Style)
seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y)
}
// 最大、最小的mark point
err := doRender(rendererList...)
if err != nil {

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,6 @@ import (
"sort"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
)
type ChartOption struct {
@ -62,8 +61,24 @@ type ChartOption struct {
RadarIndicators []RadarIndicator
// The background color of chart
BackgroundColor Color
// The flag for show symbol of line, set this to *false will hide symbol
SymbolShow *bool
// The stroke width of line chart
LineStrokeWidth float64
// The bar with of bar chart
BarWidth int
// The margin of each bar
BarMargin int
// The bar height of horizontal bar chart
BarHeight int
// Fill the area of line chart
FillArea bool
// background fill (alpha) opacity
Opacity uint8
// The child charts
Children []ChartOption
// The value formatter
ValueFormatter ValueFormatter
}
// OptionFunc option function
@ -108,9 +123,12 @@ func TitleOptionFunc(title TitleOption) OptionFunc {
}
// TitleTextOptionFunc set title text of chart
func TitleTextOptionFunc(text string) OptionFunc {
func TitleTextOptionFunc(text string, subtext ...string) OptionFunc {
return func(opt *ChartOption) {
opt.Title.Text = text
if len(subtext) != 0 {
opt.Title.Subtext = subtext[0]
}
}
}
@ -255,7 +273,10 @@ func (o *ChartOption) fillDefault() {
o.font, _ = GetFont(o.FontFamily)
if o.font == nil {
o.font, _ = chart.GetDefaultFont()
o.font, _ = GetDefaultFont()
} else {
// 如果指定了字体,则设置主题的字体
t.SetFont(o.font)
}
if o.BackgroundColor.IsZero() {
o.BackgroundColor = t.GetBackgroundColor()
@ -366,8 +387,11 @@ func TableOptionRender(opt TableChartOption) (*Painter, error) {
if opt.Width <= 0 {
opt.Width = defaultChartWidth
}
if opt.FontFamily != "" {
opt.Font, _ = GetFont(opt.FontFamily)
}
if opt.Font == nil {
opt.Font, _ = chart.GetDefaultFont()
opt.Font, _ = GetDefaultFont()
}
p, err := NewPainter(PainterOptions{

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,10 @@ package charts
import (
"errors"
"math"
"sort"
"git.smarteching.com/zeni/go-chart/v2"
)
const labelFontSize = 10
@ -49,6 +52,18 @@ func SetDefaultHeight(height int) {
}
}
var nullValue = math.MaxFloat64
// SetNullValue sets the null value, default is MaxFloat64
func SetNullValue(v float64) {
nullValue = v
}
// GetNullValue gets the null value
func GetNullValue() float64 {
return nullValue
}
type Renderer interface {
Render() (Box, error)
}
@ -110,14 +125,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
p = p.Child(PainterPaddingOption(opt.Padding))
}
legendHeight := 0
if len(opt.LegendOption.Data) != 0 {
if opt.LegendOption.Theme == nil {
opt.LegendOption.Theme = opt.Theme
}
_, err := NewLegendPainter(p, opt.LegendOption).Render()
legendResult, err := NewLegendPainter(p, opt.LegendOption).Render()
if err != nil {
return nil, err
}
legendHeight = legendResult.Height()
}
// 如果有标题
@ -131,9 +148,15 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
if err != nil {
return nil, err
}
top := chart.MaxInt(legendHeight, titleBox.Height())
// 如果是垂直方式则不计算legend高度
if opt.LegendOption.Orient == OrientVertical {
top = titleBox.Height()
}
p = p.Child(PainterPaddingOption(Box{
// 标题下留白
Top: titleBox.Height() + 20,
Top: top + 20,
}))
}
@ -163,21 +186,26 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
if len(opt.YAxisOptions) > index {
yAxisOption = opt.YAxisOptions[index]
}
divideCount := yAxisOption.DivideCount
if divideCount <= 0 {
divideCount = defaultAxisDivideCount
}
max, min := opt.SeriesList.GetMaxMin(index)
if yAxisOption.Min != nil {
min = *yAxisOption.Min
}
if yAxisOption.Max != nil {
max = *yAxisOption.Max
}
r := NewRange(AxisRangeOption{
Min: min,
Max: max,
Painter: p,
Min: min,
Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
DivideCount: defaultAxisDivideCount,
DivideCount: divideCount,
})
if yAxisOption.Min != nil && *yAxisOption.Min <= min {
r.min = *yAxisOption.Min
}
if yAxisOption.Max != nil && *yAxisOption.Max >= max {
r.max = *yAxisOption.Max
}
result.axisRanges[index] = r
if yAxisOption.Theme == nil {
@ -187,7 +215,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
yAxisOption.Data = r.Values()
} else {
yAxisOption.isCategoryAxis = true
opt.XAxis.Data = r.Values()
// 由于x轴为value部分因此计算其label单独处理
opt.XAxis.Data = NewRange(AxisRangeOption{
Painter: p,
Min: min,
Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
DivideCount: defaultAxisDivideCount,
}).Values()
opt.XAxis.isValueAxis = true
}
reverseStringSlice(yAxisOption.Data)
@ -264,6 +301,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
opt.Parent = p
}
p := opt.Parent
if opt.ValueFormatter != nil {
p.valueFormatter = opt.ValueFormatter
}
if !opt.Box.IsZero() {
p = p.Child(PainterBoxOption(opt.Box))
}
@ -306,9 +346,8 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
TitleOption: opt.Title,
LegendOption: opt.Legend,
axisReversed: axisReversed,
}
if isChild {
renderOpt.backgroundIsFilled = true
// 前置已设置背景色
backgroundIsFilled: true,
}
if len(pieSeriesList) != 0 ||
len(radarSeriesList) != 0 ||
@ -320,6 +359,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
},
}
}
if len(horizontalBarSeriesList) != 0 {
renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data)
renderOpt.YAxisOptions[0].Unit = 1
}
renderResult, err := defaultRender(p, renderOpt)
if err != nil {
@ -332,9 +375,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
if len(barSeriesList) != 0 {
handler.Add(func() error {
_, err := NewBarChart(p, BarChartOption{
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
BarWidth: opt.BarWidth,
BarMargin: opt.BarMargin,
}).render(renderResult, barSeriesList)
return err
})
@ -346,6 +391,8 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
_, err := NewHorizontalBarChart(p, HorizontalBarChartOption{
Theme: opt.theme,
Font: opt.font,
BarHeight: opt.BarHeight,
BarMargin: opt.BarMargin,
YAxisOptions: opt.YAxisOptions,
}).render(renderResult, horizontalBarSeriesList)
return err
@ -367,9 +414,13 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
if len(lineSeriesList) != 0 {
handler.Add(func() error {
_, err := NewLineChart(p, LineChartOption{
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
SymbolShow: opt.SymbolShow,
StrokeWidth: opt.LineStrokeWidth,
FillArea: opt.FillArea,
Opacity: opt.Opacity,
}).render(renderResult, lineSeriesList)
return err
})

View file

@ -26,7 +26,7 @@ import (
"errors"
"testing"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
func BenchmarkMultiChartPNGRender(b *testing.B) {

View file

@ -29,7 +29,7 @@ import (
"regexp"
"strconv"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
func convertToArray(data []byte) []byte {
@ -344,6 +344,11 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
Data: NewSeriesDataFromValues(dataItem.Value.values),
Max: item.Max,
Min: item.Min,
Label: SeriesLabel{
Color: parseColor(item.Label.Color),
Show: item.Label.Show,
Distance: item.Label.Distance,
},
})
}
continue

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,73 @@
package main
import (
"os"
"path/filepath"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
tmpPath := "./tmp"
err := os.MkdirAll(tmpPath, 0700)
if err != nil {
return err
}
file := filepath.Join(tmpPath, "area-line-chart.png")
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
return nil
}
func main() {
values := [][]float64{
{
120,
132,
101,
134,
90,
230,
210,
},
}
p, err := charts.LineRender(
values,
charts.TitleTextOptionFunc("Line"),
charts.XAxisDataOptionFunc([]string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
}),
charts.LegendLabelsOptionFunc([]string{
"Email",
}, "50"),
func(opt *charts.ChartOption) {
opt.Legend.Padding = charts.Box{
Top: 5,
Bottom: 10,
}
opt.FillArea = true
},
)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
err = writeFile(buf)
if err != nil {
panic(err)
}
}

View file

@ -1,11 +1,10 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "bar-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}

View file

@ -2,10 +2,11 @@ package main
import (
"bytes"
"fmt"
"net/http"
"strconv"
charts "github.com/vicanso/go-charts/v2"
charts "git.smarteching.com/zeni/go-charts/v2"
)
var html = `<!DOCTYPE html>
@ -261,6 +262,35 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
},
},
{
Title: charts.TitleOption{
Text: "Line Area",
},
Legend: charts.NewLegendOption([]string{
"Email",
}),
XAxis: charts.NewXAxisOption([]string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
}),
SeriesList: []charts.Series{
charts.NewSeriesFromValues([]float64{
120,
132,
101,
134,
90,
230,
210,
}),
},
FillArea: true,
},
// 柱状图
{
Title: charts.TitleOption{
@ -325,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Value: 180,
},
},
Label: charts.SeriesLabel{
Show: true,
Position: charts.PositionBottom,
},
},
},
},
@ -1935,5 +1969,6 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/echarts", echartsHandler)
fmt.Println("http://127.0.0.1:3012/")
http.ListenAndServe(":3012", nil)
}

View file

@ -5,7 +5,7 @@ import (
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +16,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "chinese-line-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@ -25,7 +25,8 @@ func writeFile(buf []byte) error {
func main() {
// 字体文件需要自行下载
buf, err := ioutil.ReadFile("../NotoSansSC.ttf")
// https://github.com/googlefonts/noto-cjk
buf, err := ioutil.ReadFile("./NotoSansSC.ttf")
if err != nil {
panic(err)
}
@ -33,6 +34,8 @@ func main() {
if err != nil {
panic(err)
}
font, _ := charts.GetFont("noto")
charts.SetDefaultFont(font)
values := [][]float64{
{
@ -83,8 +86,7 @@ func main() {
}
p, err := charts.LineRender(
values,
charts.TitleTextOptionFunc("Line"),
charts.FontFamilyOptionFunc("noto"),
charts.TitleTextOptionFunc("测试"),
charts.XAxisDataOptionFunc([]string{
"星期一",
"星期二",

View file

@ -1,11 +1,10 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "funnel-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@ -30,6 +29,8 @@ func main() {
60,
40,
20,
10,
0,
}
p, err := charts.FunnelRender(
values,
@ -40,6 +41,8 @@ func main() {
"Visit",
"Inquiry",
"Order",
"Pay",
"Cancel",
}),
)
if err != nil {

View file

@ -1,11 +1,10 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "horizontal-bar-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@ -26,20 +25,22 @@ func writeFile(buf []byte) error {
func main() {
values := [][]float64{
{
18203,
23489,
29034,
104970,
131744,
630230,
10,
30,
50,
70,
90,
110,
130,
},
{
19325,
23438,
31000,
121594,
134141,
681807,
20,
40,
60,
80,
100,
120,
140,
},
}
p, err := charts.HorizontalBarRender(
@ -56,6 +57,7 @@ func main() {
"2012",
}),
charts.YAxisDataOptionFunc([]string{
"UN",
"Brazil",
"Indonesia",
"USA",
@ -63,6 +65,9 @@ func main() {
"China",
"World",
}),
func(opt *charts.ChartOption) {
opt.SeriesList[0].RoundRadius = 5
},
)
if err != nil {
panic(err)

View file

@ -1,11 +1,11 @@
package main
import (
"io/ioutil"
"fmt"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +16,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "line-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@ -29,7 +29,8 @@ func main() {
120,
132,
101,
134,
// 134,
charts.GetNullValue(),
90,
230,
210,
@ -89,7 +90,23 @@ func main() {
"Video Ads",
"Direct",
"Search Engine",
}, charts.PositionCenter),
}, "50"),
func(opt *charts.ChartOption) {
opt.Legend.Padding = charts.Box{
Top: 5,
Bottom: 10,
}
opt.YAxisOptions = []charts.YAxisOption{
{
SplitLineShow: charts.FalseFlag(),
},
}
opt.SymbolShow = charts.FalseFlag()
opt.LineStrokeWidth = 1
opt.ValueFormatter = func(f float64) string {
return fmt.Sprintf("%.0f", f)
}
},
)
if err != nil {

View file

@ -1,12 +1,11 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
charts "github.com/vicanso/go-charts/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
charts "git.smarteching.com/zeni/go-charts/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func writeFile(buf []byte) error {
@ -17,7 +16,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "painter.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}

View file

@ -1,11 +1,10 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "pie-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}

View file

@ -1,11 +1,10 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/vicanso/go-charts/v2"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
}
file := filepath.Join(tmpPath, "radar-chart.png")
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}

View file

@ -1,14 +1,13 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/vicanso/go-charts/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-charts/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func writeFile(buf []byte, filename string) error {
@ -19,7 +18,7 @@ func writeFile(buf []byte, filename string) error {
}
file := filepath.Join(tmpPath, filename)
err = ioutil.WriteFile(file, buf, 0600)
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}

View file

@ -0,0 +1,81 @@
package main
import (
"crypto/rand"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
tmpPath := "./tmp"
err := os.MkdirAll(tmpPath, 0700)
if err != nil {
return err
}
file := filepath.Join(tmpPath, "time-line-chart.png")
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
return nil
}
func main() {
xAxisValue := []string{}
values := []float64{}
now := time.Now()
firstAxis := 0
for i := 0; i < 300; i++ {
// 设置首个axis为xx:00的时间点
if firstAxis == 0 && now.Minute() == 0 {
firstAxis = i
}
xAxisValue = append(xAxisValue, now.Format("15:04"))
now = now.Add(time.Minute)
value, _ := rand.Int(rand.Reader, big.NewInt(100))
values = append(values, float64(value.Int64()))
}
p, err := charts.LineRender(
[][]float64{
values,
},
charts.TitleTextOptionFunc("Line"),
charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()),
charts.LegendLabelsOptionFunc([]string{
"Demo",
}, "50"),
func(opt *charts.ChartOption) {
opt.XAxis.FirstAxis = firstAxis
// 必须要比计算得来的最小值更大(每60分钟)
opt.XAxis.SplitNumber = 60
opt.Legend.Padding = charts.Box{
Top: 5,
Bottom: 10,
}
opt.SymbolShow = charts.FalseFlag()
opt.LineStrokeWidth = 1
opt.ValueFormatter = func(f float64) string {
return fmt.Sprintf("%.0f", f)
}
},
)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
err = writeFile(buf)
if err != nil {
panic(err)
}
}

21
font.go
View file

@ -27,14 +27,18 @@ import (
"sync"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/roboto"
"git.smarteching.com/zeni/go-chart/v2/roboto"
)
var fonts = sync.Map{}
var ErrFontNotExists = errors.New("font is not exists")
var defaultFontFamily = "defaultFontFamily"
func init() {
_ = InstallFont("roboto", roboto.Roboto)
name := "roboto"
_ = InstallFont(name, roboto.Roboto)
font, _ := GetFont(name)
SetDefaultFont(font)
}
// InstallFont installs the font for charts
@ -47,6 +51,19 @@ func InstallFont(fontFamily string, data []byte) error {
return nil
}
// GetDefaultFont get default font
func GetDefaultFont() (*truetype.Font, error) {
return GetFont(defaultFontFamily)
}
// SetDefaultFont set default font
func SetDefaultFont(font *truetype.Font) {
if font == nil {
return
}
fonts.Store(defaultFontFamily, font)
}
// GetFont get the font by font family
func GetFont(fontFamily string) (*truetype.Font, error) {
value, ok := fonts.Load(fontFamily)

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/roboto"
"git.smarteching.com/zeni/go-chart/v2/roboto"
)
func TestInstallFont(t *testing.T) {

View file

@ -23,9 +23,6 @@
package charts
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype"
)
@ -95,13 +92,23 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
y := 0
widthList := make([]int, len(seriesList))
textList := make([]string, len(seriesList))
seriesNames := seriesList.Names()
offset := max - min
for index, item := range seriesList {
value := item.Data[0].Value
widthPercent := (value - min) / (max - min)
// 最大最小值一致则为100%
widthPercent := 100.0
if offset != 0 {
widthPercent = (value - min) / offset
}
w := int(widthPercent * float64(width))
widthList[index] = w
p := humanize.CommafWithDigits(value/max*100, 2) + "%"
textList[index] = fmt.Sprintf("%s(%s)", item.Name, p)
// 如果最大值为0则占比100%
percent := 1.0
if max != 0 {
percent = value / max
}
textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent)
}
for index, w := range widthList {

12
go.mod
View file

@ -1,17 +1,17 @@
module github.com/vicanso/go-charts/v2
module git.smarteching.com/zeni/go-charts/v2
go 1.17
go 1.24.1
require (
github.com/dustin/go-humanize v1.0.0
git.smarteching.com/zeni/go-chart/v2 v2.1.4
github.com/dustin/go-humanize v1.0.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/stretchr/testify v1.7.2
github.com/wcharczuk/go-chart/v2 v2.1.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect
golang.org/x/image v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

22
go.sum
View file

@ -1,23 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q=
git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestGrid(t *testing.T) {

View file

@ -24,7 +24,7 @@ package charts
import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type horizontalBarChart struct {
@ -48,7 +48,10 @@ type HorizontalBarChartOption struct {
// The option of title
Title TitleOption
// The legend option
Legend LegendOption
Legend LegendOption
BarHeight int
// Margin of bar
BarMargin int
}
// NewHorizontalBarChart returns a horizontal bar chart renderer
@ -80,24 +83,46 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
margin = 5
barMargin = 3
}
if opt.BarMargin > 0 {
barMargin = opt.BarMargin
}
seriesCount := len(seriesList)
// 总的高度-两个margin-(总数-1)的barMargin
barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount
if opt.BarHeight > 0 && opt.BarHeight < barHeight {
barHeight = opt.BarHeight
margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2
}
theme := opt.Theme
max, min := seriesList.GetMaxMin(0)
xRange := NewRange(AxisRangeOption{
Painter: p,
Min: min,
Max: max,
DivideCount: defaultAxisDivideCount,
Size: seriesPainter.Width(),
})
seriesNames := seriesList.Names()
rendererList := []Renderer{}
for index := range seriesList {
series := seriesList[index]
seriesColor := theme.GetSeriesColor(series.index)
divideValues := yRange.AutoDivide()
var labelPainter *SeriesLabelPainter
if series.Label.Show {
labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
P: seriesPainter,
SeriesNames: seriesNames,
Label: series.Label,
Theme: opt.Theme,
Font: opt.Font,
})
rendererList = append(rendererList, labelPainter)
}
for j, item := range series.Data {
if j >= yRange.divideCount {
continue
@ -116,16 +141,57 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
fillColor = item.Style.FillColor
}
right := w
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
})
if series.RoundRadius <= 0 {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
})
} else {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).RoundedRect(chart.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
}, series.RoundRadius)
}
// 如果label不需要展示则返回
if labelPainter == nil {
continue
}
labelValue := LabelValue{
Orient: OrientHorizontal,
Index: index,
Value: item.Value,
X: right,
Y: y + barHeight>>1,
Offset: series.Label.Offset,
FontColor: series.Label.Color,
FontSize: series.Label.FontSize,
}
if series.Label.Position == PositionLeft {
labelValue.X = 0
if labelValue.FontColor.IsZero() {
if isLightColor(fillColor) {
labelValue.FontColor = defaultLightFontColor
} else {
labelValue.FontColor = defaultDarkFontColor
}
}
}
labelPainter.Add(labelValue)
}
}
err := doRender(rendererList...)
if err != nil {
return BoxZero, err
}
return p.box, nil
}

File diff suppressed because one or more lines are too long

View file

@ -59,6 +59,8 @@ type LegendOption struct {
FontColor Color
// The flag for show legend, set this to *false will hide legend
Show *bool
// The padding of legend
Padding Box
}
// NewLegendOption returns a legend option
@ -111,9 +113,11 @@ func (l *legendPainter) Render() (Box, error) {
if opt.Left == "" {
opt.Left = PositionCenter
}
p := l.p.Child(PainterPaddingOption(Box{
Top: 5,
}))
padding := opt.Padding
if padding.IsZero() {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
p.SetTextStyle(Style{
FontSize: opt.FontSize,
FontColor: opt.FontColor,
@ -146,6 +150,8 @@ func (l *legendPainter) Render() (Box, error) {
width += item.Width()
}
}
// 增加padding
itemMaxHeight += 10
if opt.Orient == OrientVertical {
width = maxTextWidth + textOffset + legendWidth
height = offset * len(opt.Data)
@ -180,6 +186,7 @@ func (l *legendPainter) Render() (Box, error) {
x := int(left)
y := int(top) + 10
startY := y
x0 := x
y0 := y
@ -201,15 +208,21 @@ func (l *legendPainter) Render() (Box, error) {
}
return left + legendWidth
}
lastIndex := len(opt.Data) - 1
for index, text := range opt.Data {
color := theme.GetSeriesColor(index)
p.SetDrawingStyle(Style{
FillColor: color,
StrokeColor: color,
})
if x0+measureList[index].Width() > p.Width() {
itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth
if lastIndex == index {
itemWidth = x0 + measureList[index].Width() + legendWidth
}
if itemWidth > p.Width() {
x0 = 0
y0 += itemMaxHeight
y += itemMaxHeight
y0 = y
}
if opt.Align != AlignRight {
x0 = drawIcon(y0, x0)
@ -219,7 +232,7 @@ func (l *legendPainter) Render() (Box, error) {
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
x0 = drawIcon(0, x0)
x0 = drawIcon(y0, x0)
}
if opt.Orient == OrientVertical {
y0 += offset
@ -228,10 +241,11 @@ func (l *legendPainter) Render() (Box, error) {
x0 += offset
y0 = y
}
height = y0 - startY + 10
}
return Box{
Right: width,
Bottom: height,
Bottom: height + padding.Bottom + padding.Top,
}, nil
}

View file

@ -23,8 +23,10 @@
package charts
import (
"math"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
type lineChart struct {
@ -60,8 +62,16 @@ type LineChartOption struct {
Title TitleOption
// The legend option
Legend LegendOption
// The flag for show symbol of line, set this to *false will hide symbol
SymbolShow *bool
// The stroke width of line
StrokeWidth float64
// Fill the area of line
FillArea bool
// background is filled
backgroundIsFilled bool
// background fill (alpha) opacity
Opacity uint8
}
func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@ -93,25 +103,82 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
markPointPainter,
markLinePainter,
}
strokeWidth := opt.StrokeWidth
if strokeWidth == 0 {
strokeWidth = defaultStrokeWidth
}
seriesNames := seriesList.Names()
for index := range seriesList {
series := seriesList[index]
seriesColor := opt.Theme.GetSeriesColor(series.index)
drawingStyle := Style{
StrokeColor: seriesColor,
StrokeWidth: defaultStrokeWidth,
StrokeWidth: strokeWidth,
}
if len(series.Style.StrokeDashArray) > 0 {
drawingStyle.StrokeDashArray = series.Style.StrokeDashArray
}
seriesPainter.SetDrawingStyle(drawingStyle)
yRange := result.axisRanges[series.AxisIndex]
points := make([]Point, 0)
var labelPainter *SeriesLabelPainter
if series.Label.Show {
labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
P: seriesPainter,
SeriesNames: seriesNames,
Label: series.Label,
Theme: opt.Theme,
Font: opt.Font,
})
rendererList = append(rendererList, labelPainter)
}
for i, item := range series.Data {
h := yRange.getRestHeight(item.Value)
if item.Value == nullValue {
h = int(math.MaxInt32)
}
p := Point{
X: xValues[i],
Y: h,
}
points = append(points, p)
// 如果label不需要展示则返回
if labelPainter == nil {
continue
}
labelPainter.Add(LabelValue{
Index: index,
Value: item.Value,
X: p.X,
Y: p.Y,
// 字体大小
FontSize: series.Label.FontSize,
})
}
// 如果需要填充区域
if opt.FillArea {
areaPoints := make([]Point, len(points))
copy(areaPoints, points)
bottomY := yRange.getRestHeight(yRange.min)
var opacity uint8 = 200
if opt.Opacity != 0 {
opacity = opt.Opacity
}
areaPoints = append(areaPoints, Point{
X: areaPoints[len(areaPoints)-1].X,
Y: bottomY,
}, Point{
X: areaPoints[0].X,
Y: bottomY,
}, areaPoints[0])
seriesPainter.SetDrawingStyle(Style{
FillColor: seriesColor.WithAlpha(opacity),
})
seriesPainter.FillArea(areaPoints)
}
seriesPainter.SetDrawingStyle(drawingStyle)
// 画线
seriesPainter.LineStroke(points)
@ -123,7 +190,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
}
drawingStyle.StrokeWidth = 1
seriesPainter.SetDrawingStyle(drawingStyle)
seriesPainter.Dots(points)
if !isFalse(opt.SymbolShow) {
seriesPainter.Dots(points)
}
markPointPainter.Add(markPointRenderOption{
FillColor: seriesColor,
Font: opt.Font,

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,6 @@ package charts
import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
)
// NewMarkLine returns a series mark line
@ -75,7 +74,7 @@ func (m *markLinePainter) Render() (Box, error) {
}
font := opt.Font
if font == nil {
font, _ = chart.GetDefaultFont()
font, _ = GetDefaultFont()
}
summary := s.Summary()
for _, markLine := range s.MarkLine.Data {

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestMarkLine(t *testing.T) {
@ -55,6 +55,7 @@ func TestMarkLine(t *testing.T) {
StrokeColor: drawing.ColorBlack,
Series: series,
Range: NewRange(AxisRangeOption{
Painter: p,
Min: 0,
Max: 5,
Size: p.Height(),
@ -67,7 +68,7 @@ func TestMarkLine(t *testing.T) {
}
return p.Bytes()
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<circle cx=\"23\" cy=\"290\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 290\nL 562 290\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 285\nL 578 290\nL 562 295\nL 567 290\nL 562 285\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text><circle cx=\"23\" cy=\"320\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 320\nL 562 320\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 315\nL 578 320\nL 562 325\nL 567 320\nL 562 315\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"324\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><circle cx=\"23\" cy=\"350\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 350\nL 562 350\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 345\nL 578 350\nL 562 355\nL 567 350\nL 562 345\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"354\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<circle cx=\"23\" cy=\"272\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 272\nL 562 272\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 267\nL 578 272\nL 562 277\nL 567 272\nL 562 267\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"276\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text><circle cx=\"23\" cy=\"308\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 308\nL 562 308\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 303\nL 578 308\nL 562 313\nL 567 308\nL 562 303\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"312\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><circle cx=\"23\" cy=\"344\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 344\nL 562 344\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 339\nL 578 344\nL 562 349\nL 567 344\nL 562 339\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"348\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text></svg>",
},
}
for _, tt := range tests {

View file

@ -24,7 +24,6 @@ package charts
import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/drawing"
)
// NewMarkPoint returns a series mark point
@ -78,16 +77,15 @@ func (m *markPointPainter) Render() (Box, error) {
symbolSize = 30
}
textStyle := Style{
FontColor: drawing.Color{
R: 238,
G: 238,
B: 238,
A: 255,
},
FontSize: labelFontSize,
StrokeWidth: 1,
Font: opt.Font,
}
if isLightColor(opt.FillColor) {
textStyle.FontColor = defaultLightFontColor
} else {
textStyle.FontColor = defaultDarkFontColor
}
painter.OverrideDrawingStyle(Style{
FillColor: opt.FillColor,
}).OverrideTextStyle(textStyle)

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestMarkPoint(t *testing.T) {

View file

@ -28,9 +28,11 @@ import (
"math"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type ValueFormatter func(float64) string
type Painter struct {
render chart.Renderer
box Box
@ -39,7 +41,8 @@ type Painter struct {
style Style
theme ColorPalette
// 类型
outputType string
outputType string
valueFormatter ValueFormatter
}
type PainterOptions struct {
@ -56,6 +59,8 @@ type PainterOptions struct {
type PainterOption func(*Painter)
type TicksOption struct {
// the first tick
First int
Length int
Orient string
Count int
@ -68,6 +73,11 @@ type MultiTextOption struct {
Unit int
Position string
Align string
// The text rotation of label
TextRotation float64
Offset Box
// The first text index
First int
}
type GridOption struct {
@ -146,7 +156,7 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
}
font := opts.Font
if font == nil {
f, err := chart.GetDefaultFont()
f, err := GetDefaultFont()
if err != nil {
return nil, err
}
@ -188,6 +198,9 @@ func (p *Painter) setOptions(opts ...PainterOption) {
func (p *Painter) Child(opt ...PainterOption) *Painter {
child := &Painter{
// 格式化
valueFormatter: p.valueFormatter,
// render
render: p.render,
box: p.box.Clone(),
font: p.font,
@ -438,11 +451,18 @@ func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) {
}
func (p *Painter) LineStroke(points []Point) *Painter {
shouldMoveTo := false
for index, point := range points {
x := point.X
y := point.Y
if index == 0 {
if y == int(math.MaxInt32) {
p.Stroke()
shouldMoveTo = true
continue
}
if shouldMoveTo || index == 0 {
p.MoveTo(x, y)
shouldMoveTo = false
} else {
p.LineTo(x, y)
}
@ -545,6 +565,19 @@ func (p *Painter) Text(body string, x, y int) *Painter {
return p
}
func (p *Painter) TextRotation(body string, x, y int, radians float64) {
p.render.SetTextRotation(radians)
p.render.Text(body, x+p.box.Left, y+p.box.Top)
p.render.ClearTextRotation()
}
func (p *Painter) SetTextRotation(radians float64) {
p.render.SetTextRotation(radians)
}
func (p *Painter) ClearTextRotation() {
p.render.ClearTextRotation()
}
func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box {
style := p.style
textWarp := style.TextWrap
@ -587,6 +620,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
return p
}
count := opt.Count
first := opt.First
width := p.Width()
height := p.Height()
unit := 1
@ -601,7 +635,10 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
values = autoDivide(width, count)
}
for index, value := range values {
if index%unit != 0 {
if index < first {
continue
}
if (index-first)%unit != 0 {
continue
}
if isVertical {
@ -637,12 +674,15 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
}
count := len(opt.TextList)
positionCenter := true
showIndex := opt.Unit / 2
if containsString([]string{
PositionLeft,
PositionTop,
}, opt.Position) {
positionCenter = false
count--
// 非居中
showIndex = 0
}
width := p.Width()
height := p.Height()
@ -653,11 +693,19 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
} else {
values = autoDivide(width, count)
}
showIndex := opt.Unit / 2
isTextRotation := opt.TextRotation != 0
offset := opt.Offset
for index, text := range opt.TextList {
if opt.Unit != 0 && index%opt.Unit != showIndex {
if index < opt.First {
continue
}
if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex {
continue
}
if isTextRotation {
p.ClearTextRotation()
p.SetTextRotation(opt.TextRotation)
}
box := p.MeasureText(text)
start := values[index]
if positionCenter {
@ -678,8 +726,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
} else {
x = start - box.Width()>>1
}
x += offset.Left
y += offset.Top
p.Text(text, x, y)
}
if isTextRotation {
p.ClearTextRotation()
}
return p
}
@ -750,6 +803,48 @@ func (p *Painter) Rect(box Box) *Painter {
return p
}
func (p *Painter) RoundedRect(box Box, radius int) *Painter {
r := (box.Right - box.Left) / 2
if radius > r {
radius = r
}
rx := float64(radius)
ry := float64(radius)
p.MoveTo(box.Left+radius, box.Top)
p.LineTo(box.Right-radius, box.Top)
cx := box.Right - radius
cy := box.Top + radius
// right top
p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2)
p.LineTo(box.Right, box.Bottom-radius)
// right bottom
cx = box.Right - radius
cy = box.Bottom - radius
p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2)
p.LineTo(box.Left+radius, box.Bottom)
// left bottom
cx = box.Left + radius
cy = box.Bottom - radius
p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2)
p.LineTo(box.Left, box.Top+radius)
// left top
cx = box.Left + radius
cy = box.Top + radius
p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2)
p.Close()
p.FillStroke()
p.Fill()
return p
}
func (p *Painter) LegendLineDot(box Box) *Painter {
width := box.Width()
height := box.Height()
@ -765,3 +860,7 @@ func (p *Painter) LegendLineDot(box Box) *Painter {
p.FillStroke()
return p
}
func (p *Painter) GetRenderer() chart.Renderer {
return p.render
}

View file

@ -28,8 +28,8 @@ import (
"github.com/golang/freetype/truetype"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestPainterOption(t *testing.T) {
@ -343,6 +343,29 @@ func TestPainter(t *testing.T) {
}
}
func TestRoundedRect(t *testing.T) {
assert := assert.New(t)
p, err := NewPainter(PainterOptions{
Width: 400,
Height: 300,
Type: ChartOutputSVG,
})
assert.Nil(err)
p.OverrideDrawingStyle(Style{
FillColor: drawing.ColorWhite,
StrokeWidth: 1,
StrokeColor: drawing.ColorWhite,
}).RoundedRect(Box{
Left: 10,
Right: 30,
Bottom: 150,
Top: 10,
}, 5)
buf, err := p.Bytes()
assert.Nil(err)
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 15 10\nL 25 10\nL 25 10\nA 5 5 90.00 0 1 30 15\nL 30 145\nL 30 145\nA 5 5 90.00 0 1 25 150\nL 15 150\nL 15 150\nA 5 5 90.00 0 1 10 145\nL 10 15\nL 10 15\nA 5 5 90.00 0 1 15 10\nZ\" style=\"stroke-width:1;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(buf))
}
func TestPainterTextFit(t *testing.T) {
assert := assert.New(t)
p, err := NewPainter(PainterOptions{
@ -351,7 +374,7 @@ func TestPainterTextFit(t *testing.T) {
Type: ChartOutputSVG,
})
assert.Nil(err)
f, _ := chart.GetDefaultFont()
f, _ := GetDefaultFont()
style := Style{
FontSize: 12,
FontColor: chart.ColorBlack,

View file

@ -27,7 +27,7 @@ import (
"math"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type pieChart struct {
@ -63,6 +63,96 @@ func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
}
}
type sector struct {
value float64
percent float64
cx int
cy int
rx float64
ry float64
start float64
delta float64
offset int
quadrant int
lineStartX int
lineStartY int
lineBranchX int
lineBranchY int
lineEndX int
lineEndY int
showLabel bool
label string
series Series
color Color
}
func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector {
s := sector{}
s.value = value
s.percent = value / totalValue
s.cx = cx
s.cy = cy
s.rx = radius
s.ry = radius
p := (currentValue + value/2) / totalValue
if p < 0.25 {
s.quadrant = 1
} else if p < 0.5 {
s.quadrant = 4
} else if p < 0.75 {
s.quadrant = 3
} else {
s.quadrant = 2
}
s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2
s.delta = chart.PercentToRadians(value / totalValue)
angle := s.start + s.delta/2
s.lineStartX = cx + int(radius*math.Cos(angle))
s.lineStartY = cy + int(radius*math.Sin(angle))
s.lineBranchX = cx + int(labelRadius*math.Cos(angle))
s.lineBranchY = cy + int(labelRadius*math.Sin(angle))
s.offset = labelLineLength
if s.lineBranchX <= cx {
s.offset *= -1
}
s.lineEndX = s.lineBranchX + s.offset
s.lineEndY = s.lineBranchY
s.series = series
s.color = color
s.showLabel = series.Label.Show
s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent)
return s
}
func (s *sector) calculateY(prevY int) int {
for i := 0; i <= s.cy; i++ {
if s.quadrant <= 2 {
if (prevY - s.lineBranchY) > labelFontSize+5 {
break
}
s.lineBranchY -= 1
} else {
if (s.lineBranchY - prevY) > labelFontSize+5 {
break
}
s.lineBranchY += 1
}
}
s.lineEndY = s.lineBranchY
return s.lineBranchY
}
func (s *sector) calculateTextXY(textBox Box) (x int, y int) {
textMargin := 3
x = s.lineEndX + textMargin
y = s.lineEndY + textBox.Height()>>1 - 1
if s.offset < 0 {
textWidth := textBox.Width()
x = s.lineEndX - textWidth - textMargin
}
return
}
func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
opt := p.opt
values := make([]float64, len(seriesList))
@ -101,79 +191,103 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
theme := opt.Theme
currentValue := float64(0)
prevEndX := 0
prevEndY := 0
var quadrant1, quadrant2, quadrant3, quadrant4 []sector
for index, v := range values {
series := seriesList[index]
color := theme.GetSeriesColor(index)
if index == len(values)-1 {
if color == theme.GetSeriesColor(0) {
color = theme.GetSeriesColor(1)
}
}
s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color)
switch quadrant := s.quadrant; quadrant {
case 1:
quadrant1 = append([]sector{s}, quadrant1...)
case 2:
quadrant2 = append(quadrant2, s)
case 3:
quadrant3 = append([]sector{s}, quadrant3...)
case 4:
quadrant4 = append(quadrant4, s)
}
currentValue += v
}
sectors := append(quadrant1, quadrant4...)
sectors = append(sectors, quadrant3...)
sectors = append(sectors, quadrant2...)
currentQuadrant := 0
prevY := 0
maxY := 0
minY := 0
for _, s := range sectors {
seriesPainter.OverrideDrawingStyle(Style{
StrokeWidth: 1,
StrokeColor: theme.GetSeriesColor(index),
FillColor: theme.GetSeriesColor(index),
StrokeColor: s.color,
FillColor: s.color,
})
seriesPainter.MoveTo(cx, cy)
start := chart.PercentToRadians(currentValue/total) - math.Pi/2
currentValue += v
percent := (v / total)
delta := chart.PercentToRadians(percent)
seriesPainter.ArcTo(cx, cy, radius, radius, start, delta).
LineTo(cx, cy).
Close().
FillStroke()
series := seriesList[index]
// 是否显示label
showLabel := series.Label.Show
if !showLabel {
seriesPainter.MoveTo(s.cx, s.cy)
seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke()
if !s.showLabel {
continue
}
// label的角度为饼块中间
angle := start + delta/2
startx := cx + int(radius*math.Cos(angle))
starty := cy + int(radius*math.Sin(angle))
endx := cx + int(labelRadius*math.Cos(angle))
endy := cy + int(labelRadius*math.Sin(angle))
// 计算是否有重叠如果有则调整y坐标位置
if index != 0 &&
math.Abs(float64(endx-prevEndX)) < labelFontSize &&
math.Abs(float64(endy-prevEndY)) < labelFontSize {
endy -= (labelFontSize << 1)
if currentQuadrant != s.quadrant {
if s.quadrant == 1 {
minY = cy * 2
maxY = 0
prevY = cy * 2
}
if s.quadrant == 2 {
if currentQuadrant != 3 {
prevY = s.lineEndY
} else {
prevY = minY
}
}
if s.quadrant == 3 {
if currentQuadrant != 4 {
prevY = s.lineEndY
} else {
minY = cy * 2
maxY = 0
prevY = 0
}
}
if s.quadrant == 4 {
if currentQuadrant != 1 {
prevY = s.lineEndY
} else {
prevY = maxY
}
}
currentQuadrant = s.quadrant
}
prevEndX = endx
prevEndY = endy
seriesPainter.MoveTo(startx, starty)
seriesPainter.LineTo(endx, endy)
offset := labelLineWidth
if endx < cx {
offset *= -1
prevY = s.calculateY(prevY)
if prevY > maxY {
maxY = prevY
}
seriesPainter.MoveTo(endx, endy)
endx += offset
seriesPainter.LineTo(endx, endy)
if prevY < minY {
minY = prevY
}
seriesPainter.MoveTo(s.lineStartX, s.lineStartY)
seriesPainter.LineTo(s.lineBranchX, s.lineBranchY)
seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY)
seriesPainter.LineTo(s.lineEndX, s.lineEndY)
seriesPainter.Stroke()
textStyle := Style{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
}
if !series.Label.Color.IsZero() {
textStyle.FontColor = series.Label.Color
if !s.series.Label.Color.IsZero() {
textStyle.FontColor = s.series.Label.Color
}
seriesPainter.OverrideTextStyle(textStyle)
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
textBox := seriesPainter.MeasureText(text)
textMargin := 3
x := endx + textMargin
y := endy + textBox.Height()>>1 - 1
if offset < 0 {
textWidth := textBox.Width()
x = endx - textWidth - textMargin
}
seriesPainter.Text(text, x, y)
x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label))
seriesPainter.Text(s.label, x, y)
}
return p.p.box, nil
}

File diff suppressed because one or more lines are too long

View file

@ -25,9 +25,10 @@ package charts
import (
"errors"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
type radarChart struct {
@ -200,7 +201,11 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
continue
}
indicator := indicators[j]
percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
var percent float64
offset := indicator.Max - indicator.Min
if offset > 0 {
percent = (item.Value - indicator.Min) / offset
}
r := percent * radius
p := getPolygonPoint(center, r, angles[j])
linePoints = append(linePoints, p)
@ -226,9 +231,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
StrokeColor: color,
FillColor: dotFillColor,
})
for _, point := range linePoints {
for index, point := range linePoints {
seriesPainter.Circle(dotWith, point.X, point.Y)
seriesPainter.FillStroke()
if series.Label.Show && index < len(series.Data) {
value := humanize.FtoaWithDigits(series.Data[index].Value, 2)
b := seriesPainter.MeasureText(value)
seriesPainter.Text(value, point.X-b.Width()/2, point.Y)
}
}
}

View file

@ -29,6 +29,7 @@ import (
const defaultAxisDivideCount = 6
type axisRange struct {
p *Painter
divideCount int
min float64
max float64
@ -37,6 +38,7 @@ type axisRange struct {
}
type AxisRangeOption struct {
Painter *Painter
// The min value of axis
Min float64
// The max value of axis
@ -60,7 +62,10 @@ func NewRange(opt AxisRangeOption) axisRange {
r := math.Abs(max - min)
// 最小单位计算
unit := 2
unit := 1
if r > 5 {
unit = 2
}
if r > 10 {
unit = 4
}
@ -85,7 +90,12 @@ func NewRange(opt AxisRangeOption) axisRange {
}
}
max = min + float64(unit*divideCount)
expectMax := opt.Max * 2
if max > expectMax {
max = float64(ceilFloatToInt(expectMax))
}
return axisRange{
p: opt.Painter,
divideCount: divideCount,
min: min,
max: max,
@ -98,15 +108,22 @@ func NewRange(opt AxisRangeOption) axisRange {
func (r axisRange) Values() []string {
offset := (r.max - r.min) / float64(r.divideCount)
values := make([]string, 0)
formatter := commafWithDigits
if r.p != nil && r.p.valueFormatter != nil {
formatter = r.p.valueFormatter
}
for i := 0; i <= r.divideCount; i++ {
v := r.min + float64(i)*offset
value := commafWithDigits(v)
value := formatter(v)
values = append(values, value)
}
return values
}
func (r *axisRange) getHeight(value float64) int {
if r.max <= r.min {
return 0
}
v := (value - r.min) / (r.max - r.min)
return int(v * float64(r.size))
}

View file

@ -26,7 +26,7 @@ import (
"strings"
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2"
)
type SeriesData struct {
@ -79,6 +79,12 @@ type SeriesLabel struct {
Show bool
// Distance to the host graphic element.
Distance int
// The position of label
Position string
// The offset of label's position
Offset Box
// The font size of label
FontSize float64
}
const (
@ -120,6 +126,8 @@ type Series struct {
Name string
// Radius for Pie chart, e.g.: 40%, default is "40%"
Radius string
// Round for bar chart
RoundRadius int
// Mark point for series
MarkPoint SeriesMarkPoint
// Make line for series
@ -165,6 +173,10 @@ func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) {
continue
}
for _, item := range series.Data {
// 如果为空值,忽略
if item.Value == nullValue {
continue
}
if item.Value > max {
max = item.Value
}
@ -269,6 +281,14 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
return NewLabelFormatter(seriesNames, layout)
}
// NewFunnelLabelFormatter returns a funner label formatter
func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter {
if len(layout) == 0 {
layout = "{b}({d})"
}
return NewLabelFormatter(seriesNames, layout)
}
// NewValueLabelFormatter returns a value formatter
func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter {
if len(layout) == 0 {

148
series_label.go Normal file
View file

@ -0,0 +1,148 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
)
type labelRenderValue struct {
Text string
Style Style
X int
Y int
// 旋转
Radians float64
}
type LabelValue struct {
Index int
Value float64
X int
Y int
// 旋转
Radians float64
// 字体颜色
FontColor Color
// 字体大小
FontSize float64
Orient string
Offset Box
}
type SeriesLabelPainter struct {
p *Painter
seriesNames []string
label *SeriesLabel
theme ColorPalette
font *truetype.Font
values []labelRenderValue
}
type SeriesLabelPainterParams struct {
P *Painter
SeriesNames []string
Label SeriesLabel
Theme ColorPalette
Font *truetype.Font
}
func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter {
return &SeriesLabelPainter{
p: params.P,
seriesNames: params.SeriesNames,
label: &params.Label,
theme: params.Theme,
font: params.Font,
values: make([]labelRenderValue, 0),
}
}
func (o *SeriesLabelPainter) Add(value LabelValue) {
label := o.label
distance := label.Distance
if distance == 0 {
distance = 5
}
text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1)
labelStyle := Style{
FontColor: o.theme.GetTextColor(),
FontSize: labelFontSize,
Font: o.font,
}
if value.FontSize != 0 {
labelStyle.FontSize = value.FontSize
}
if !value.FontColor.IsZero() {
label.Color = value.FontColor
}
if !label.Color.IsZero() {
labelStyle.FontColor = label.Color
}
p := o.p
p.OverrideDrawingStyle(labelStyle)
rotated := value.Radians != 0
if rotated {
p.SetTextRotation(value.Radians)
}
textBox := p.MeasureText(text)
renderValue := labelRenderValue{
Text: text,
Style: labelStyle,
X: value.X,
Y: value.Y,
Radians: value.Radians,
}
if value.Orient != OrientHorizontal {
renderValue.X -= textBox.Width() >> 1
renderValue.Y -= distance
} else {
renderValue.X += distance
renderValue.Y += textBox.Height() >> 1
renderValue.Y -= 2
}
if rotated {
renderValue.X = value.X + textBox.Width()>>1 - 1
p.ClearTextRotation()
} else {
if textBox.Width()%2 != 0 {
renderValue.X++
}
}
renderValue.X += value.Offset.Left
renderValue.Y += value.Offset.Top
o.values = append(o.values, renderValue)
}
func (o *SeriesLabelPainter) Render() (Box, error) {
for _, item := range o.values {
o.p.OverrideTextStyle(item.Style)
if item.Radians != 0 {
o.p.TextRotation(item.Text, item.X, item.Y, item.Radians)
} else {
o.p.Text(item.Text, item.X, item.Y)
}
}
return chart.BoxZero, nil
}

254
start_zh.md Normal file
View file

@ -0,0 +1,254 @@
# go-charts
`go-charts`主要分为了下几个模块:
- `标题`:图表的标题,包括主副标题,位置为图表的顶部
- `图例`:图表的图例列表,用于标识每个图例对应的颜色与名称信息,默认为图表的顶部,可自定义位置
- `X轴`图表的x轴用于折线图、柱状图中表示每个点对应的时间位置图表的底部
- `Y轴`图表的y轴用于折线图、柱状图中最多可使用两组y轴一左一右默认位置图表的左侧
- `内容`: 图表的内容,折线图、柱状图、饼图等,在图表的中间区域
## 标题
### 常用设置
标题一般仅需要设置主副标题即可,其它的属性均会设置默认值,常用的方式是使用`TitleTextOptionFunc`设置,其中副标题为可选值,方式如下:
```go
charts.TitleTextOptionFunc("Text", "Subtext"),
```
### 个性化设置
```go
func(opt *charts.ChartOption) {
opt.Title = charts.TitleOption{
// 主标题
Text: "Text",
// 副标题
Subtext: "Subtext",
// 标题左侧位置,可设置为"center""right",数值("20")或百份比("20%")
Left: charts.PositionRight,
// 标题顶部位置,只可调为数值
Top: "20",
// 主标题文字大小
FontSize: 14,
// 副标题文字大小
SubtextFontSize: 12,
// 主标题字体颜色
FontColor: charts.Color{
R: 100,
G: 100,
B: 100,
A: 255,
},
// 副标题字体影响
SubtextFontColor: charts.Color{
R: 200,
G: 200,
B: 200,
A: 255,
},
}
},
```
### 部分属性个性化设置
```go
charts.TitleTextOptionFunc("Text", "Subtext"),
func(opt *charts.ChartOption) {
// 修改top的值
opt.Title.Top = "20"
},
```
## 图例
### 常用设置
图例组件与图表中的数据一一对应,常用仅设置其名称及左侧的值即可(可选),方式如下:
```go
charts.LegendLabelsOptionFunc([]string{
"Email",
"Union Ads",
"Video Ads",
"Direct",
"Search Engine",
}, "50"),
```
### 个性化设置
```go
func(opt *charts.ChartOption) {
opt.Legend = charts.LegendOption{
// 图例名称
Data: []string{
"Email",
"Union Ads",
"Video Ads",
"Direct",
"Search Engine",
},
// 图例左侧位置,可设置为"center""right",数值("20")或百份比("20%")
// 如果示例有多行,只影响第一行,而且对于多行的示例,设置"center", "right"无效
Left: "50",
// 图例顶部位置,只可调为数值
Top: "10",
// 图例图标的位置,默认为左侧,只允许左或右
Align: charts.AlignRight,
// 图例排列方式,默认为水平,只允许水平或垂直
Orient: charts.OrientVertical,
// 图标类型,提供"rect"与"lineDot"两种类型
Icon: charts.IconRect,
// 字体大小
FontSize: 14,
// 字体颜色
FontColor: charts.Color{
R: 150,
G: 150,
B: 150,
A: 255,
},
// 是否展示,如果不需要展示则设置
// Show: charts.FalseFlag(),
// 图例区域的padding值
Padding: charts.Box{
Top: 10,
Left: 10,
},
}
},
```
### 部分属性个性化设置
```go
charts.LegendLabelsOptionFunc([]string{
"Email",
"Union Ads",
"Video Ads",
"Direct",
"Search Engine",
}, "50"),
func(opt *charts.ChartOption) {
opt.Legend.Top = "10"
},
```
## X轴
### 常用设置
图表中X轴的展示常用的设置方式是指定数组即可
```go
charts.XAxisDataOptionFunc([]string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
}),
```
### 个性化设置
```go
func(opt *charts.ChartOption) {
opt.XAxis = charts.XAxisOption{
// X轴内容
Data: []string{
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
},
// 如果数据点不居中则设置为false
BoundaryGap: charts.FalseFlag(),
// 字体大小
FontSize: 14,
// 是否展示,如果不需要展示则设置
// Show: charts.FalseFlag(),
// 会根据文本内容以及此值选择适合的分块大小,一般不需要设置
// SplitNumber: 3,
// 线条颜色
StrokeColor: charts.Color{
R: 200,
G: 200,
B: 200,
A: 255,
},
// 文字颜色
FontColor: charts.Color{
R: 100,
G: 100,
B: 100,
A: 255,
},
}
},
```
### 部分属性个性化设置
```go
charts.XAxisDataOptionFunc([]string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
}),
func(opt *charts.ChartOption) {
opt.XAxis.FontColor = charts.Color{
R: 100,
G: 100,
B: 100,
A: 255,
},
},
```
## Y轴
图表中的y轴展示的相关数据会根据图表中的数据自动生成适合的值如果需要自定义则可自定义以下部分数据
```go
func(opt *charts.ChartOption) {
opt.YAxisOptions = []charts.YAxisOption{
{
// 字体大小
FontSize: 16,
// 字体颜色
FontColor: charts.Color{
R: 100,
G: 100,
B: 100,
A: 255,
},
// 内容,{value}会替换为对应的值
Formatter: "{value} ml",
// Y轴颜色如果设置此值会覆盖font color
Color: charts.Color{
R: 255,
G: 0,
B: 0,
A: 255,
},
},
}
},
```

View file

@ -26,8 +26,8 @@ import (
"errors"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
type tableChart struct {

View file

@ -24,8 +24,7 @@ package charts
import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
const ThemeDark = "dark"
@ -36,12 +35,19 @@ const ThemeAnt = "ant"
type ColorPalette interface {
IsDark() bool
GetAxisStrokeColor() Color
SetAxisStrokeColor(Color)
GetAxisSplitLineColor() Color
SetAxisSplitLineColor(Color)
GetSeriesColor(int) Color
SetSeriesColor([]Color)
GetBackgroundColor() Color
SetBackgroundColor(Color)
GetTextColor() Color
SetTextColor(Color)
GetFontSize() float64
SetFontSize(float64)
GetFont() *truetype.Font
SetFont(*truetype.Font)
}
type themeColorPalette struct {
@ -64,12 +70,25 @@ type ThemeOption struct {
SeriesColors []Color
}
var palettes = map[string]ColorPalette{}
var palettes = map[string]*themeColorPalette{}
const defaultFontSize = 12.0
var defaultTheme ColorPalette
var defaultLightFontColor = drawing.Color{
R: 70,
G: 70,
B: 70,
A: 255,
}
var defaultDarkFontColor = drawing.Color{
R: 238,
G: 238,
B: 238,
A: 255,
}
func init() {
echartSeriesColors := []Color{
parseColor("#5470c6"),
@ -241,7 +260,8 @@ func NewTheme(name string) ColorPalette {
if !ok {
p = palettes[ThemeLight]
}
return p
clone := *p
return &clone
}
func (t *themeColorPalette) IsDark() bool {
@ -252,23 +272,42 @@ func (t *themeColorPalette) GetAxisStrokeColor() Color {
return t.axisStrokeColor
}
func (t *themeColorPalette) SetAxisStrokeColor(c Color) {
t.axisStrokeColor = c
}
func (t *themeColorPalette) GetAxisSplitLineColor() Color {
return t.axisSplitLineColor
}
func (t *themeColorPalette) SetAxisSplitLineColor(c Color) {
t.axisSplitLineColor = c
}
func (t *themeColorPalette) GetSeriesColor(index int) Color {
colors := t.seriesColors
return colors[index%len(colors)]
}
func (t *themeColorPalette) SetSeriesColor(colors []Color) {
t.seriesColors = colors
}
func (t *themeColorPalette) GetBackgroundColor() Color {
return t.backgroundColor
}
func (t *themeColorPalette) SetBackgroundColor(c Color) {
t.backgroundColor = c
}
func (t *themeColorPalette) GetTextColor() Color {
return t.textColor
}
func (t *themeColorPalette) SetTextColor(c Color) {
t.textColor = c
}
func (t *themeColorPalette) GetFontSize() float64 {
if t.fontSize != 0 {
return t.fontSize
@ -276,10 +315,18 @@ func (t *themeColorPalette) GetFontSize() float64 {
return defaultFontSize
}
func (t *themeColorPalette) SetFontSize(fontSize float64) {
t.fontSize = fontSize
}
func (t *themeColorPalette) GetFont() *truetype.Font {
if t.font != nil {
return t.font
}
f, _ := chart.GetDefaultFont()
f, _ := GetDefaultFont()
return f
}
func (t *themeColorPalette) SetFont(f *truetype.Font) {
t.font = f
}

View file

@ -36,10 +36,6 @@ type TitleOption struct {
Text string
// Subtitle text, support \n for new line
Subtext string
// // Title style
// Style Style
// // Subtitle style
// SubtextStyle Style
// Distance between title component and the left side of the container.
// It can be pixel value: 20, percentage value: 20%,
// or position value: right, center.

33
util.go
View file

@ -29,8 +29,8 @@ import (
"strings"
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TrueFlag() *bool {
@ -160,15 +160,25 @@ func NewFloatPoint(f float64) *float64 {
v := f
return &v
}
const K_VALUE = float64(1000)
const M_VALUE = K_VALUE * K_VALUE
const G_VALUE = M_VALUE * K_VALUE
const T_VALUE = G_VALUE * K_VALUE
func commafWithDigits(value float64) string {
decimals := 2
m := float64(1000 * 1000)
if value >= m {
return humanize.CommafWithDigits(value/m, decimals) + "M"
if value >= T_VALUE {
return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T"
}
k := float64(1000)
if value >= k {
return humanize.CommafWithDigits(value/k, decimals) + "k"
if value >= G_VALUE {
return humanize.CommafWithDigits(value/G_VALUE, decimals) + "G"
}
if value >= M_VALUE {
return humanize.CommafWithDigits(value/M_VALUE, decimals) + "M"
}
if value >= K_VALUE {
return humanize.CommafWithDigits(value/K_VALUE, decimals) + "k"
}
return humanize.CommafWithDigits(value, decimals)
}
@ -252,3 +262,10 @@ func getPolygonPoints(center Point, radius float64, sides int) []Point {
}
return points
}
func isLightColor(c Color) bool {
r := float64(c.R) * float64(c.R) * 0.299
g := float64(c.G) * float64(c.G) * 0.587
b := float64(c.B) * float64(c.B) * 0.114
return math.Sqrt(r+g+b) > 127.5
}

View file

@ -26,8 +26,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestGetDefaultInt(t *testing.T) {
@ -189,3 +189,35 @@ func TestParseColor(t *testing.T) {
A: 250,
}, c)
}
func TestIsLightColor(t *testing.T) {
assert := assert.New(t)
assert.True(isLightColor(drawing.Color{
R: 255,
G: 255,
B: 255,
}))
assert.True(isLightColor(drawing.Color{
R: 145,
G: 204,
B: 117,
}))
assert.False(isLightColor(drawing.Color{
R: 88,
G: 112,
B: 198,
}))
assert.False(isLightColor(drawing.Color{
R: 0,
G: 0,
B: 0,
}))
assert.False(isLightColor(drawing.Color{
R: 16,
G: 12,
B: 42,
}))
}

View file

@ -47,7 +47,13 @@ type XAxisOption struct {
// The line color of axis
StrokeColor Color
// The color of label
FontColor Color
FontColor Color
// The text rotation of label
TextRotation float64
// The first axis
FirstAxis int
// The offset of label
LabelOffset Box
isValueAxis bool
}
@ -81,6 +87,9 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
FontColor: opt.FontColor,
Show: opt.Show,
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
TextRotation: opt.TextRotation,
LabelOffset: opt.LabelOffset,
FirstAxis: opt.FirstAxis,
}
if opt.isValueAxis {
axisOpt.SplitLineShow = true

View file

@ -47,7 +47,11 @@ type YAxisOption struct {
Color Color
// The flag for show axis, set this to *false will hide axis
Show *bool
DivideCount int
Unit int
isCategoryAxis bool
// The flag for show axis split line, set this to true will show axis split line
SplitLineShow *bool
}
// NewYAxisOptions returns a y axis option
@ -87,6 +91,7 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
SplitLineShow: true,
SplitLineColor: theme.GetAxisSplitLineColor(),
Show: opt.Show,
Unit: opt.Unit,
}
if !opt.Color.IsZero() {
axisOpt.FontColor = opt.Color
@ -97,6 +102,9 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
axisOpt.StrokeWidth = 1
axisOpt.SplitLineShow = false
}
if opt.SplitLineShow != nil {
axisOpt.SplitLineShow = *opt.SplitLineShow
}
return axisOpt
}