Compare commits
93 commits
v2.0.0-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 958172a1f1 | |||
| 0eacc8e394 | |||
|
|
d25a827706 | ||
|
|
5842c71b1d | ||
|
|
e7dc4189d5 | ||
|
|
32e6dd52d0 | ||
|
|
9614835723 | ||
|
|
9b7634c2c2 | ||
|
|
5a69c3e5a3 | ||
|
|
8c6c4e007c | ||
|
|
765febd03a | ||
|
|
19a4d783fd | ||
|
|
06fe1006d5 | ||
|
|
f1a231ff4b | ||
|
|
c7c0655113 | ||
|
|
310800a5f0 | ||
|
|
e09ab2c3c7 | ||
|
|
c2f709a742 | ||
|
|
98af9866a4 | ||
|
|
c302d0ffa4 | ||
|
|
8bcb584aba | ||
|
|
0ddb9e4ef1 | ||
|
|
18d8ee51fb | ||
|
|
687baad0af | ||
|
|
a158191faf | ||
|
|
c810369730 | ||
|
|
19173dfd37 | ||
|
|
e7a49c2c21 | ||
|
|
20e8d4a078 | ||
|
|
29a5ece545 | ||
|
|
d3f7a773af | ||
|
|
8ba9e2e1b2 | ||
|
|
e10175594b | ||
|
|
b3cb5a75cb | ||
|
|
a767b3e1af | ||
|
|
830d4bdd21 | ||
|
|
d5533447f5 | ||
|
|
ef04ac14ab | ||
|
|
f9a534ea02 | ||
|
|
df6180e59a | ||
|
|
5f0aec60d3 | ||
|
|
6db8e2c8dc | ||
|
|
4fc250aefc | ||
|
|
55eca7b0b9 | ||
|
|
a42d0727df | ||
|
|
7e1f003be8 | ||
|
|
de4250f60b | ||
|
|
2ed86a81d0 | ||
|
|
6f6d6c3447 | ||
|
|
bdcc871ab1 | ||
|
|
a88e607bfc | ||
|
|
74a47a9858 | ||
|
|
0a1061a8db | ||
|
|
6652ece0fe | ||
|
|
0a80e7056f | ||
|
|
1f5b9d513e | ||
|
|
de49ef8c68 | ||
|
|
825e65d930 | ||
|
|
50605907c7 | ||
|
|
bb9af986be | ||
|
|
4a1ff80556 | ||
|
|
128d5b2774 | ||
|
|
dc1a89d3ff | ||
|
|
93e03856ca | ||
|
|
550b9874d2 | ||
|
|
e530adccb6 | ||
|
|
817fceff73 | ||
|
|
e095223705 | ||
|
|
1713bc283f | ||
|
|
cac6fd03d3 | ||
|
|
3d20bea846 | ||
|
|
8740c55a1a | ||
|
|
3af0d4d445 | ||
|
|
b5b2d37e87 | ||
|
|
805f4381a3 | ||
|
|
959377542e | ||
|
|
c220b10ae6 | ||
|
|
0a3ac7096a | ||
|
|
eef3a2f97b | ||
|
|
b56d0c5460 | ||
|
|
c862467a5b | ||
|
|
f483e2a850 | ||
|
|
d53fa1a329 | ||
|
|
0eecb6c5b7 | ||
|
|
aed2250cb8 | ||
|
|
93eec00bbe | ||
|
|
f1276067d7 | ||
|
|
da3ad16c23 | ||
|
|
b3a3018ea2 | ||
|
|
2fb0ebcbf7 | ||
|
|
8c5647f65f | ||
|
|
706896737b | ||
|
|
92458aece2 |
64 changed files with 3884 additions and 387 deletions
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -16,3 +16,5 @@
|
|||
*.png
|
||||
*.svg
|
||||
tmp
|
||||
NotoSansSC.ttf
|
||||
.vscode
|
||||
89
README.md
89
README.md
|
|
@ -1,11 +1,13 @@
|
|||
# go-charts
|
||||
|
||||
Clone from https://github.com/vicanso/go-charts
|
||||
|
||||
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
|
||||
[](https://github.com/vicanso/go-charts/actions)
|
||||
|
||||
[中文](./README_zh.md)
|
||||
|
||||
`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`.
|
||||
`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`.
|
||||
|
||||
`Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`.
|
||||
|
||||
|
|
@ -15,9 +17,13 @@ Screenshot of common charts, the left part is light theme, the right part is gra
|
|||
<img src="./assets/go-charts.png" alt="go-charts">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/go-table.png" alt="go-table">
|
||||
</p>
|
||||
|
||||
## Chart Type
|
||||
|
||||
These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
|
||||
These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`.
|
||||
|
||||
## Example
|
||||
|
||||
|
|
@ -29,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() {
|
||||
|
|
@ -95,7 +101,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -170,7 +176,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -227,7 +233,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -282,7 +288,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -340,7 +346,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -374,13 +380,78 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
### Table
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
header := []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
}
|
||||
data := [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
}
|
||||
spans := map[int]int{
|
||||
0: 2,
|
||||
1: 1,
|
||||
// 设置第三列的span
|
||||
2: 3,
|
||||
3: 2,
|
||||
4: 2,
|
||||
}
|
||||
p, err := charts.TableRender(
|
||||
header,
|
||||
data,
|
||||
spans,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// snip...
|
||||
}
|
||||
```
|
||||
|
||||
### ECharts Render
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
88
README_zh.md
88
README_zh.md
|
|
@ -3,7 +3,7 @@
|
|||
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
|
||||
[](https://github.com/vicanso/go-charts/actions)
|
||||
|
||||
`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。
|
||||
`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。
|
||||
|
||||
`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana):
|
||||
|
||||
|
|
@ -12,9 +12,13 @@
|
|||
<img src="./assets/go-charts.png" alt="go-charts">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/go-table.png" alt="go-table">
|
||||
</p
|
||||
|
||||
## 支持图表类型
|
||||
|
||||
支持以下的图表类型:`line`, `bar`, `pie`, `radar` 以及 `funnel`
|
||||
支持以下的图表类型:`line`, `bar`, `horizontal bar`, `pie`, `radar`, `funnel` 以及 `table`
|
||||
|
||||
|
||||
## 示例
|
||||
|
|
@ -28,7 +32,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
charts "github.com/vicanso/go-charts/v2"
|
||||
charts "git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -94,7 +98,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -169,7 +173,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -226,7 +230,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -281,7 +285,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -339,7 +343,7 @@ func main() {
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -373,13 +377,77 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
### Table
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
header := []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
}
|
||||
data := [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
}
|
||||
spans := map[int]int{
|
||||
0: 2,
|
||||
1: 1,
|
||||
// 设置第三列的span
|
||||
2: 3,
|
||||
3: 2,
|
||||
4: 2,
|
||||
}
|
||||
p, err := charts.TableRender(
|
||||
header,
|
||||
data,
|
||||
spans,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// snip...
|
||||
}
|
||||
```
|
||||
### ECharts Render
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -501,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)
|
||||
|
|
|
|||
4
alias.go
4
alias.go
|
|
@ -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
|
||||
|
|
|
|||
BIN
assets/go-table.png
Normal file
BIN
assets/go-table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
54
axis.go
54
axis.go
|
|
@ -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,9 +159,31 @@ 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)
|
||||
textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth))
|
||||
unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber)))
|
||||
if isTextRotation {
|
||||
top.ClearTextRotation()
|
||||
}
|
||||
|
||||
// 增加30px来计算文本展示区域
|
||||
textFillWidth := float64(textMaxWidth + 20)
|
||||
// 根据文本宽度计算较为符合的展示项
|
||||
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
|
||||
height := 0
|
||||
|
|
@ -228,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
Length: tickLength,
|
||||
Unit: unit,
|
||||
Orient: orient,
|
||||
First: opt.FirstAxis,
|
||||
})
|
||||
p.LineStroke([]Point{
|
||||
{
|
||||
|
|
@ -246,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()
|
||||
|
|
@ -263,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,
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
},
|
||||
// 顶部
|
||||
{
|
||||
|
|
|
|||
118
bar_chart.go
118
bar_chart.go
|
|
@ -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 {
|
||||
|
|
@ -32,6 +34,7 @@ type barChart struct {
|
|||
opt *BarChartOption
|
||||
}
|
||||
|
||||
// NewBarChart returns a bar chart renderer
|
||||
func NewBarChart(p *Painter, opt BarChartOption) *barChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -43,6 +46,7 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart {
|
|||
}
|
||||
|
||||
type BarChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -57,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) {
|
||||
|
|
@ -73,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(),
|
||||
})
|
||||
|
|
@ -89,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()
|
||||
|
|
@ -102,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]
|
||||
|
|
@ -110,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
|
||||
|
|
@ -127,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{
|
||||
// 居中的位置
|
||||
|
|
@ -148,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 := NewValueLabelFormater(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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -190,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
|
|
@ -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()
|
||||
|
|
@ -336,3 +357,70 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) {
|
|||
SeriesList: seriesList,
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// TableRender table chart render
|
||||
func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) {
|
||||
opt := TableChartOption{
|
||||
Header: header,
|
||||
Data: data,
|
||||
}
|
||||
if len(spanMaps) != 0 {
|
||||
spanMap := spanMaps[0]
|
||||
spans := make([]int, len(opt.Header))
|
||||
for index := range spans {
|
||||
v, ok := spanMap[index]
|
||||
if !ok {
|
||||
v = 1
|
||||
}
|
||||
spans[index] = v
|
||||
}
|
||||
opt.Spans = spans
|
||||
}
|
||||
return TableOptionRender(opt)
|
||||
}
|
||||
|
||||
// TableOptionRender table render with option
|
||||
func TableOptionRender(opt TableChartOption) (*Painter, error) {
|
||||
if opt.Type == "" {
|
||||
opt.Type = ChartOutputPNG
|
||||
}
|
||||
if opt.Width <= 0 {
|
||||
opt.Width = defaultChartWidth
|
||||
}
|
||||
if opt.FontFamily != "" {
|
||||
opt.Font, _ = GetFont(opt.FontFamily)
|
||||
}
|
||||
if opt.Font == nil {
|
||||
opt.Font, _ = GetDefaultFont()
|
||||
}
|
||||
|
||||
p, err := NewPainter(PainterOptions{
|
||||
Type: opt.Type,
|
||||
Width: opt.Width,
|
||||
// 仅用于计算表格高度,因此随便设置即可
|
||||
Height: 100,
|
||||
Font: opt.Font,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := NewTableChart(p, opt).render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err = NewPainter(PainterOptions{
|
||||
Type: opt.Type,
|
||||
Width: info.Width,
|
||||
Height: info.Height,
|
||||
Font: opt.Font,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = NewTableChart(p, opt).renderWithInfo(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
97
charts.go
97
charts.go
|
|
@ -24,27 +24,46 @@ package charts
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
const labelFontSize = 10
|
||||
const smallLabelFontSize = 8
|
||||
const defaultDotWidth = 2.0
|
||||
const defaultStrokeWidth = 2.0
|
||||
|
||||
var defaultChartWidth = 600
|
||||
var defaultChartHeight = 400
|
||||
|
||||
// SetDefaultWidth sets default width of chart
|
||||
func SetDefaultWidth(width int) {
|
||||
if width > 0 {
|
||||
defaultChartWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefaultHeight sets default height of chart
|
||||
func SetDefaultHeight(height int) {
|
||||
if height > 0 {
|
||||
defaultChartHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -106,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()
|
||||
}
|
||||
|
||||
// 如果有标题
|
||||
|
|
@ -127,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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -159,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 {
|
||||
|
|
@ -183,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)
|
||||
|
|
@ -260,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))
|
||||
}
|
||||
|
|
@ -302,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 ||
|
||||
|
|
@ -316,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 {
|
||||
|
|
@ -328,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
|
||||
})
|
||||
|
|
@ -342,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
|
||||
|
|
@ -363,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
|
||||
})
|
||||
|
|
|
|||
255
charts_test.go
Normal file
255
charts_test.go
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
)
|
||||
|
||||
func BenchmarkMultiChartPNGRender(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
opt := ChartOption{
|
||||
Type: ChartOutputPNG,
|
||||
Legend: LegendOption{
|
||||
Top: "-90",
|
||||
Data: []string{
|
||||
"Milk Tea",
|
||||
"Matcha Latte",
|
||||
"Cheese Cocoa",
|
||||
"Walnut Brownie",
|
||||
},
|
||||
},
|
||||
Padding: chart.Box{
|
||||
Top: 100,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
Left: 10,
|
||||
},
|
||||
XAxis: NewXAxisOption([]string{
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
}),
|
||||
YAxisOptions: []YAxisOption{
|
||||
{
|
||||
|
||||
Min: NewFloatPoint(0),
|
||||
Max: NewFloatPoint(90),
|
||||
},
|
||||
},
|
||||
SeriesList: []Series{
|
||||
NewSeriesFromValues([]float64{
|
||||
56.5,
|
||||
82.1,
|
||||
88.7,
|
||||
70.1,
|
||||
53.4,
|
||||
85.1,
|
||||
}),
|
||||
NewSeriesFromValues([]float64{
|
||||
51.1,
|
||||
51.4,
|
||||
55.1,
|
||||
53.3,
|
||||
73.8,
|
||||
68.7,
|
||||
}),
|
||||
NewSeriesFromValues([]float64{
|
||||
40.1,
|
||||
62.2,
|
||||
69.5,
|
||||
36.4,
|
||||
45.2,
|
||||
32.5,
|
||||
}, ChartTypeBar),
|
||||
NewSeriesFromValues([]float64{
|
||||
25.2,
|
||||
37.1,
|
||||
41.2,
|
||||
18,
|
||||
33.9,
|
||||
49.1,
|
||||
}, ChartTypeBar),
|
||||
},
|
||||
Children: []ChartOption{
|
||||
{
|
||||
Legend: LegendOption{
|
||||
Show: FalseFlag(),
|
||||
Data: []string{
|
||||
"Milk Tea",
|
||||
"Matcha Latte",
|
||||
"Cheese Cocoa",
|
||||
"Walnut Brownie",
|
||||
},
|
||||
},
|
||||
Box: chart.Box{
|
||||
Top: 20,
|
||||
Left: 400,
|
||||
Right: 500,
|
||||
Bottom: 120,
|
||||
},
|
||||
SeriesList: NewPieSeriesList([]float64{
|
||||
435.9,
|
||||
354.3,
|
||||
285.9,
|
||||
204.5,
|
||||
}, PieSeriesOption{
|
||||
Label: SeriesLabel{
|
||||
Show: true,
|
||||
},
|
||||
Radius: "35%",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
d, err := Render(opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf, err := d.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
panic(errors.New("data is nil"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMultiChartSVGRender(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
opt := ChartOption{
|
||||
Legend: LegendOption{
|
||||
Top: "-90",
|
||||
Data: []string{
|
||||
"Milk Tea",
|
||||
"Matcha Latte",
|
||||
"Cheese Cocoa",
|
||||
"Walnut Brownie",
|
||||
},
|
||||
},
|
||||
Padding: chart.Box{
|
||||
Top: 100,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
Left: 10,
|
||||
},
|
||||
XAxis: NewXAxisOption([]string{
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
}),
|
||||
YAxisOptions: []YAxisOption{
|
||||
{
|
||||
|
||||
Min: NewFloatPoint(0),
|
||||
Max: NewFloatPoint(90),
|
||||
},
|
||||
},
|
||||
SeriesList: []Series{
|
||||
NewSeriesFromValues([]float64{
|
||||
56.5,
|
||||
82.1,
|
||||
88.7,
|
||||
70.1,
|
||||
53.4,
|
||||
85.1,
|
||||
}),
|
||||
NewSeriesFromValues([]float64{
|
||||
51.1,
|
||||
51.4,
|
||||
55.1,
|
||||
53.3,
|
||||
73.8,
|
||||
68.7,
|
||||
}),
|
||||
NewSeriesFromValues([]float64{
|
||||
40.1,
|
||||
62.2,
|
||||
69.5,
|
||||
36.4,
|
||||
45.2,
|
||||
32.5,
|
||||
}, ChartTypeBar),
|
||||
NewSeriesFromValues([]float64{
|
||||
25.2,
|
||||
37.1,
|
||||
41.2,
|
||||
18,
|
||||
33.9,
|
||||
49.1,
|
||||
}, ChartTypeBar),
|
||||
},
|
||||
Children: []ChartOption{
|
||||
{
|
||||
Legend: LegendOption{
|
||||
Show: FalseFlag(),
|
||||
Data: []string{
|
||||
"Milk Tea",
|
||||
"Matcha Latte",
|
||||
"Cheese Cocoa",
|
||||
"Walnut Brownie",
|
||||
},
|
||||
},
|
||||
Box: chart.Box{
|
||||
Top: 20,
|
||||
Left: 400,
|
||||
Right: 500,
|
||||
Bottom: 120,
|
||||
},
|
||||
SeriesList: NewPieSeriesList([]float64{
|
||||
435.9,
|
||||
354.3,
|
||||
285.9,
|
||||
204.5,
|
||||
}, PieSeriesOption{
|
||||
Label: SeriesLabel{
|
||||
Show: true,
|
||||
},
|
||||
Radius: "35%",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
d, err := Render(opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf, err := d.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
panic(errors.New("data is nil"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
73
examples/area_line_chart/main.go
Normal file
73
examples/area_line_chart/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -92,6 +93,48 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
|
|||
bytesList = append(bytesList, buf)
|
||||
}
|
||||
|
||||
p, err := charts.TableOptionRender(charts.TableChartOption{
|
||||
Type: charts.ChartOutputSVG,
|
||||
Header: []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
},
|
||||
Data: [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bytesList = append(bytesList, buf)
|
||||
|
||||
data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte("")))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(data)
|
||||
|
|
@ -219,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{
|
||||
|
|
@ -283,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
|||
Value: 180,
|
||||
},
|
||||
},
|
||||
Label: charts.SeriesLabel{
|
||||
Show: true,
|
||||
Position: charts.PositionBottom,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1893,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
"星期一",
|
||||
"星期二",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
178
examples/table/main.go
Normal file
178
examples/table/main.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.smarteching.com/zeni/go-charts/v2"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
func writeFile(buf []byte, filename string) error {
|
||||
tmpPath := "./tmp"
|
||||
err := os.MkdirAll(tmpPath, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(tmpPath, filename)
|
||||
err = os.WriteFile(file, buf, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// charts.SetDefaultTableSetting(charts.TableDarkThemeSetting)
|
||||
charts.SetDefaultWidth(810)
|
||||
header := []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
}
|
||||
data := [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
}
|
||||
spans := map[int]int{
|
||||
0: 2,
|
||||
1: 1,
|
||||
// 设置第三列的span
|
||||
2: 3,
|
||||
3: 2,
|
||||
4: 2,
|
||||
}
|
||||
p, err := charts.TableRender(
|
||||
header,
|
||||
data,
|
||||
spans,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = writeFile(buf, "table.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bgColor := charts.Color{
|
||||
R: 16,
|
||||
G: 22,
|
||||
B: 30,
|
||||
A: 255,
|
||||
}
|
||||
p, err = charts.TableOptionRender(charts.TableChartOption{
|
||||
Header: []string{
|
||||
"Name",
|
||||
"Price",
|
||||
"Change",
|
||||
},
|
||||
BackgroundColor: bgColor,
|
||||
HeaderBackgroundColor: bgColor,
|
||||
RowBackgroundColors: []charts.Color{
|
||||
bgColor,
|
||||
},
|
||||
HeaderFontColor: drawing.ColorWhite,
|
||||
FontColor: drawing.ColorWhite,
|
||||
Padding: charts.Box{
|
||||
Top: 15,
|
||||
Right: 10,
|
||||
Bottom: 15,
|
||||
Left: 10,
|
||||
},
|
||||
Data: [][]string{
|
||||
{
|
||||
"Datadog Inc",
|
||||
"97.32",
|
||||
"-7.49%",
|
||||
},
|
||||
{
|
||||
"Hashicorp Inc",
|
||||
"28.66",
|
||||
"-9.25%",
|
||||
},
|
||||
{
|
||||
"Gitlab Inc",
|
||||
"51.63",
|
||||
"+4.32%",
|
||||
},
|
||||
},
|
||||
TextAligns: []string{
|
||||
"",
|
||||
charts.AlignRight,
|
||||
charts.AlignRight,
|
||||
},
|
||||
CellStyle: func(tc charts.TableCell) *charts.Style {
|
||||
column := tc.Column
|
||||
if column != 2 {
|
||||
return nil
|
||||
}
|
||||
value, _ := strconv.ParseFloat(strings.Replace(tc.Text, "%", "", 1), 64)
|
||||
if value == 0 {
|
||||
return nil
|
||||
}
|
||||
style := charts.Style{
|
||||
Padding: charts.Box{
|
||||
Bottom: 5,
|
||||
},
|
||||
}
|
||||
if value > 0 {
|
||||
style.FillColor = charts.Color{
|
||||
R: 179,
|
||||
G: 53,
|
||||
B: 20,
|
||||
A: 255,
|
||||
}
|
||||
} else if value < 0 {
|
||||
style.FillColor = charts.Color{
|
||||
R: 33,
|
||||
G: 124,
|
||||
B: 50,
|
||||
A: 255,
|
||||
}
|
||||
}
|
||||
return &style
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buf, err = p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = writeFile(buf, "table-color.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
81
examples/time_line_chart/main.go
Normal file
81
examples/time_line_chart/main.go
Normal 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
21
font.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@
|
|||
package charts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +31,7 @@ type funnelChart struct {
|
|||
opt *FunnelChartOption
|
||||
}
|
||||
|
||||
// NewFunnelSeriesList returns a series list for funnel
|
||||
func NewFunnelSeriesList(values []float64) SeriesList {
|
||||
seriesList := make(SeriesList, len(values))
|
||||
for index, value := range values {
|
||||
|
|
@ -44,6 +42,7 @@ func NewFunnelSeriesList(values []float64) SeriesList {
|
|||
return seriesList
|
||||
}
|
||||
|
||||
// NewFunnelChart returns a funnel chart renderer
|
||||
func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -55,6 +54,7 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
|
|||
}
|
||||
|
||||
type FunnelChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -92,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
12
go.mod
|
|
@ -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
22
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
26
grid.go
26
grid.go
|
|
@ -28,16 +28,27 @@ type gridPainter struct {
|
|||
}
|
||||
|
||||
type GridPainterOption struct {
|
||||
StrokeWidth float64
|
||||
StrokeColor Color
|
||||
Column int
|
||||
Row int
|
||||
IgnoreFirstRow bool
|
||||
IgnoreLastRow bool
|
||||
// The stroke width
|
||||
StrokeWidth float64
|
||||
// The stroke color
|
||||
StrokeColor Color
|
||||
// The spans of column
|
||||
ColumnSpans []int
|
||||
// The column of grid
|
||||
Column int
|
||||
// The row of grid
|
||||
Row int
|
||||
// Ignore first row
|
||||
IgnoreFirstRow bool
|
||||
// Ignore last row
|
||||
IgnoreLastRow bool
|
||||
// Ignore first column
|
||||
IgnoreFirstColumn bool
|
||||
IgnoreLastColumn bool
|
||||
// Ignore last column
|
||||
IgnoreLastColumn bool
|
||||
}
|
||||
|
||||
// NewGridPainter returns new a grid renderer
|
||||
func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
|
||||
return &gridPainter{
|
||||
p: p,
|
||||
|
|
@ -72,6 +83,7 @@ func (g *gridPainter) Render() (Box, error) {
|
|||
})
|
||||
g.p.Grid(GridOption{
|
||||
Column: opt.Column,
|
||||
ColumnSpans: opt.ColumnSpans,
|
||||
Row: opt.Row,
|
||||
IgnoreColumnLines: ignoreColumnLines,
|
||||
IgnoreRowLines: ignoreRowLines,
|
||||
|
|
|
|||
20
grid_test.go
20
grid_test.go
|
|
@ -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) {
|
||||
|
|
@ -54,6 +54,24 @@ func TestGrid(t *testing.T) {
|
|||
},
|
||||
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 100 0\nL 100 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 200 0\nL 200 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 300 0\nL 300 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 400 0\nL 400 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 500 0\nL 500 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 66\nL 600 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 133\nL 600 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 200\nL 600 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 266\nL 600 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 333\nL 600 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
|
||||
},
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewGridPainter(p, GridPainterOption{
|
||||
StrokeColor: drawing.ColorBlack,
|
||||
ColumnSpans: []int{
|
||||
2,
|
||||
5,
|
||||
3,
|
||||
},
|
||||
Row: 6,
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 0 0\nL 0 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 120 0\nL 120 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 420 0\nL 420 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 600 0\nL 600 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 0\nL 600 0\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 66\nL 600 66\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 133\nL 600 133\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 200\nL 600 200\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 266\nL 600 266\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 333\nL 600 333\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 0 400\nL 600 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
p, err := NewPainter(PainterOptions{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -33,6 +33,7 @@ type horizontalBarChart struct {
|
|||
}
|
||||
|
||||
type HorizontalBarChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -47,9 +48,13 @@ 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
|
||||
func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -78,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
|
||||
|
|
@ -114,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
40
legend.go
40
legend.go
|
|
@ -36,6 +36,7 @@ const IconRect = "rect"
|
|||
const IconLineDot = "lineDot"
|
||||
|
||||
type LegendOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// Text array of legend
|
||||
Data []string
|
||||
|
|
@ -58,8 +59,11 @@ 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
|
||||
func NewLegendOption(labels []string, left ...string) LegendOption {
|
||||
opt := LegendOption{
|
||||
Data: labels,
|
||||
|
|
@ -70,6 +74,7 @@ func NewLegendOption(labels []string, left ...string) LegendOption {
|
|||
return opt
|
||||
}
|
||||
|
||||
// IsEmpty checks legend is empty
|
||||
func (opt *LegendOption) IsEmpty() bool {
|
||||
isEmpty := true
|
||||
for _, v := range opt.Data {
|
||||
|
|
@ -81,6 +86,7 @@ func (opt *LegendOption) IsEmpty() bool {
|
|||
return isEmpty
|
||||
}
|
||||
|
||||
// NewLegendPainter returns a legend renderer
|
||||
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
||||
return &legendPainter{
|
||||
p: p,
|
||||
|
|
@ -107,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,
|
||||
|
|
@ -131,13 +139,19 @@ func (l *legendPainter) Render() (Box, error) {
|
|||
textOffset := 2
|
||||
legendWidth := 30
|
||||
legendHeight := 20
|
||||
itemMaxHeight := 0
|
||||
for _, item := range measureList {
|
||||
if item.Height() > itemMaxHeight {
|
||||
itemMaxHeight = item.Height()
|
||||
}
|
||||
if opt.Orient == OrientVertical {
|
||||
height += item.Height()
|
||||
} else {
|
||||
width += item.Width()
|
||||
}
|
||||
}
|
||||
// 增加padding
|
||||
itemMaxHeight += 10
|
||||
if opt.Orient == OrientVertical {
|
||||
width = maxTextWidth + textOffset + legendWidth
|
||||
height = offset * len(opt.Data)
|
||||
|
|
@ -166,8 +180,13 @@ func (l *legendPainter) Render() (Box, error) {
|
|||
}
|
||||
top, _ := strconv.Atoi(opt.Top)
|
||||
|
||||
if left < 0 {
|
||||
left = 0
|
||||
}
|
||||
|
||||
x := int(left)
|
||||
y := int(top) + 10
|
||||
startY := y
|
||||
x0 := x
|
||||
y0 := y
|
||||
|
||||
|
|
@ -189,12 +208,22 @@ 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,
|
||||
})
|
||||
itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth
|
||||
if lastIndex == index {
|
||||
itemWidth = x0 + measureList[index].Width() + legendWidth
|
||||
}
|
||||
if itemWidth > p.Width() {
|
||||
x0 = 0
|
||||
y += itemMaxHeight
|
||||
y0 = y
|
||||
}
|
||||
if opt.Align != AlignRight {
|
||||
x0 = drawIcon(y0, x0)
|
||||
x0 += textOffset
|
||||
|
|
@ -203,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
|
||||
|
|
@ -212,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -32,6 +34,7 @@ type lineChart struct {
|
|||
opt *LineChartOption
|
||||
}
|
||||
|
||||
// NewLineChart returns a line chart render
|
||||
func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -43,6 +46,7 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
|
|||
}
|
||||
|
||||
type LineChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -58,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) {
|
||||
|
|
@ -91,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)
|
||||
|
||||
|
|
@ -121,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
|
|
@ -24,9 +24,9 @@ package charts
|
|||
|
||||
import (
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
// NewMarkLine returns a series mark line
|
||||
func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
|
||||
data := make([]SeriesMarkData, len(markLineTypes))
|
||||
for index, t := range markLineTypes {
|
||||
|
|
@ -48,6 +48,7 @@ func (m *markLinePainter) Add(opt markLineRenderOption) {
|
|||
m.options = append(m.options, opt)
|
||||
}
|
||||
|
||||
// NewMarkLinePainter returns a mark line renderer
|
||||
func NewMarkLinePainter(p *Painter) *markLinePainter {
|
||||
return &markLinePainter{
|
||||
p: p,
|
||||
|
|
@ -73,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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// NewMarkPoint returns a series mark point
|
||||
func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
|
||||
data := make([]SeriesMarkData, len(markPointTypes))
|
||||
for index, t := range markPointTypes {
|
||||
|
|
@ -54,6 +55,7 @@ type markPointRenderOption struct {
|
|||
Points []Point
|
||||
}
|
||||
|
||||
// NewMarkPointPainter returns a mark point renderer
|
||||
func NewMarkPointPainter(p *Painter) *markPointPainter {
|
||||
return &markPointPainter{
|
||||
p: p,
|
||||
|
|
@ -63,7 +65,6 @@ func NewMarkPointPainter(p *Painter) *markPointPainter {
|
|||
|
||||
func (m *markPointPainter) Render() (Box, error) {
|
||||
painter := m.p
|
||||
theme := m.p.theme
|
||||
for _, opt := range m.options {
|
||||
s := opt.Series
|
||||
if len(s.MarkPoint.Data) == 0 {
|
||||
|
|
@ -75,15 +76,22 @@ func (m *markPointPainter) Render() (Box, error) {
|
|||
if symbolSize == 0 {
|
||||
symbolSize = 30
|
||||
}
|
||||
painter.OverrideDrawingStyle(Style{
|
||||
FillColor: opt.FillColor,
|
||||
}).OverrideTextStyle(Style{
|
||||
FontColor: theme.GetTextColor(),
|
||||
textStyle := Style{
|
||||
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)
|
||||
for _, markPointData := range s.MarkPoint.Data {
|
||||
textStyle.FontSize = labelFontSize
|
||||
painter.OverrideTextStyle(textStyle)
|
||||
p := points[summary.MinIndex]
|
||||
value := summary.MinValue
|
||||
switch markPointData.Type {
|
||||
|
|
@ -95,6 +103,11 @@ func (m *markPointPainter) Render() (Box, error) {
|
|||
painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
||||
text := commafWithDigits(value)
|
||||
textBox := painter.MeasureText(text)
|
||||
if textBox.Width() > symbolSize {
|
||||
textStyle.FontSize = smallLabelFontSize
|
||||
painter.OverrideTextStyle(textStyle)
|
||||
textBox = painter.MeasureText(text)
|
||||
}
|
||||
painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -69,7 +69,7 @@ func TestMarkPoint(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<path d=\"M 67 62\nA 15 15 330.00 1 1 73 62\nL 70 48\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><path d=\"M 55 48\nQ70,85 85,48\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><text x=\"66\" y=\"53\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text></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 67 62\nA 15 15 330.00 1 1 73 62\nL 70 48\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><path d=\"M 55 48\nQ70,85 85,48\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><text x=\"66\" y=\"53\" style=\"stroke-width:0;stroke:none;fill:rgba(238,238,238,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text></svg>",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
168
painter.go
168
painter.go
|
|
@ -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
|
||||
|
|
@ -38,6 +40,9 @@ type Painter struct {
|
|||
parent *Painter
|
||||
style Style
|
||||
theme ColorPalette
|
||||
// 类型
|
||||
outputType string
|
||||
valueFormatter ValueFormatter
|
||||
}
|
||||
|
||||
type PainterOptions struct {
|
||||
|
|
@ -54,6 +59,8 @@ type PainterOptions struct {
|
|||
type PainterOption func(*Painter)
|
||||
|
||||
type TicksOption struct {
|
||||
// the first tick
|
||||
First int
|
||||
Length int
|
||||
Orient string
|
||||
Count int
|
||||
|
|
@ -66,11 +73,17 @@ 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 {
|
||||
Column int
|
||||
Row int
|
||||
Column int
|
||||
Row int
|
||||
ColumnSpans []int
|
||||
// 忽略不展示的column
|
||||
IgnoreColumnLines []int
|
||||
// 忽略不展示的row
|
||||
|
|
@ -136,14 +149,14 @@ func PainterWidthHeightOption(width, height int) PainterOption {
|
|||
}
|
||||
}
|
||||
|
||||
// NewPainter creates a new painter
|
||||
// NewPainter creates a painter
|
||||
func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
|
||||
if opts.Width <= 0 || opts.Height <= 0 {
|
||||
return nil, errors.New("width/height can not be nil")
|
||||
}
|
||||
font := opts.Font
|
||||
if font == nil {
|
||||
f, err := chart.GetDefaultFont()
|
||||
f, err := GetDefaultFont()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -168,6 +181,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
|
|||
Bottom: opts.Height,
|
||||
},
|
||||
font: font,
|
||||
// 类型
|
||||
outputType: opts.Type,
|
||||
}
|
||||
p.setOptions(opt...)
|
||||
if p.theme == nil {
|
||||
|
|
@ -183,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,
|
||||
|
|
@ -433,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)
|
||||
}
|
||||
|
|
@ -467,7 +492,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter {
|
|||
return p
|
||||
}
|
||||
|
||||
func (p *Painter) SetBackground(width, height int, color Color) *Painter {
|
||||
func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter {
|
||||
r := p.render
|
||||
s := chart.Style{
|
||||
FillColor: color,
|
||||
|
|
@ -475,12 +500,20 @@ func (p *Painter) SetBackground(width, height int, color Color) *Painter {
|
|||
// 背景色
|
||||
p.SetDrawingStyle(s)
|
||||
defer p.ResetStyle()
|
||||
// 设置背景色不使用box,因此不直接使用Painter
|
||||
r.MoveTo(0, 0)
|
||||
r.LineTo(width, 0)
|
||||
r.LineTo(width, height)
|
||||
r.LineTo(0, height)
|
||||
r.LineTo(0, 0)
|
||||
if len(inside) != 0 && inside[0] {
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(width, 0)
|
||||
p.LineTo(width, height)
|
||||
p.LineTo(0, height)
|
||||
p.LineTo(0, 0)
|
||||
} else {
|
||||
// 设置背景色不使用box,因此不直接使用Painter
|
||||
r.MoveTo(0, 0)
|
||||
r.LineTo(width, 0)
|
||||
r.LineTo(width, height)
|
||||
r.LineTo(0, height)
|
||||
r.LineTo(0, 0)
|
||||
}
|
||||
p.FillStroke()
|
||||
return p
|
||||
}
|
||||
|
|
@ -532,7 +565,20 @@ func (p *Painter) Text(body string, x, y int) *Painter {
|
|||
return p
|
||||
}
|
||||
|
||||
func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
|
||||
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
|
||||
style.TextWrap = chart.TextWrapWord
|
||||
|
|
@ -541,11 +587,24 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
|
|||
p.SetTextStyle(style)
|
||||
var output chart.Box
|
||||
|
||||
textAlign := ""
|
||||
if len(textAligns) != 0 {
|
||||
textAlign = textAligns[0]
|
||||
}
|
||||
for index, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
x0 := x
|
||||
y0 := y + output.Height()
|
||||
p.Text(line, x0, y0)
|
||||
lineBox := r.MeasureText(line)
|
||||
switch textAlign {
|
||||
case AlignRight:
|
||||
x0 += width - lineBox.Width()
|
||||
case AlignCenter:
|
||||
x0 += (width - lineBox.Width()) >> 1
|
||||
}
|
||||
p.Text(line, x0, y0)
|
||||
output.Right = chart.MaxInt(lineBox.Right, output.Right)
|
||||
output.Bottom += lineBox.Height()
|
||||
if index < len(lines)-1 {
|
||||
|
|
@ -561,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
|
||||
|
|
@ -575,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 {
|
||||
|
|
@ -611,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()
|
||||
|
|
@ -627,10 +693,19 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
|||
} else {
|
||||
values = autoDivide(width, count)
|
||||
}
|
||||
isTextRotation := opt.TextRotation != 0
|
||||
offset := opt.Offset
|
||||
for index, text := range opt.TextList {
|
||||
if opt.Unit != 0 && index%opt.Unit != 0 {
|
||||
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 {
|
||||
|
|
@ -651,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
|
||||
}
|
||||
|
||||
|
|
@ -690,8 +770,12 @@ func (p *Painter) Grid(opt GridOption) *Painter {
|
|||
})
|
||||
}
|
||||
}
|
||||
if opt.Column > 0 {
|
||||
values := autoDivide(width, opt.Column)
|
||||
columnCount := sumInt(opt.ColumnSpans)
|
||||
if columnCount == 0 {
|
||||
columnCount = opt.Column
|
||||
}
|
||||
if columnCount > 0 {
|
||||
values := autoDivideSpans(width, columnCount, opt.ColumnSpans)
|
||||
drawLines(values, opt.IgnoreColumnLines, true)
|
||||
}
|
||||
if opt.Row > 0 {
|
||||
|
|
@ -719,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()
|
||||
|
|
@ -734,3 +860,7 @@ func (p *Painter) LegendLineDot(box Box) *Painter {
|
|||
p.FillStroke()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Painter) GetRenderer() chart.Renderer {
|
||||
return p.render
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -143,13 +143,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -165,13 +165,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -187,13 +187,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -209,13 +209,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -231,13 +231,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -253,13 +253,13 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -279,7 +279,7 @@ func TestPainter(t *testing.T) {
|
|||
fn: func(p *Painter) {
|
||||
p.SetStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
StrokeColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -297,7 +297,7 @@ func TestPainter(t *testing.T) {
|
|||
{
|
||||
fn: func(p *Painter) {
|
||||
p.SetDrawingStyle(Style{
|
||||
FillColor: drawing.Color{
|
||||
FillColor: Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
267
pie_chart.go
267
pie_chart.go
|
|
@ -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 {
|
||||
|
|
@ -36,6 +36,7 @@ type pieChart struct {
|
|||
}
|
||||
|
||||
type PieChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -51,6 +52,7 @@ type PieChartOption struct {
|
|||
backgroundIsFilled bool
|
||||
}
|
||||
|
||||
// NewPieChart returns a pie chart renderer
|
||||
func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -61,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))
|
||||
|
|
@ -97,90 +189,105 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
|||
seriesNames = seriesList.Names()
|
||||
}
|
||||
theme := opt.Theme
|
||||
if len(values) == 1 {
|
||||
|
||||
currentValue := float64(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(0),
|
||||
FillColor: theme.GetSeriesColor(0),
|
||||
StrokeColor: s.color,
|
||||
FillColor: s.color,
|
||||
})
|
||||
seriesPainter.MoveTo(cx, cy).
|
||||
Circle(radius, cx, cy)
|
||||
} else {
|
||||
currentValue := float64(0)
|
||||
prevEndX := 0
|
||||
prevEndY := 0
|
||||
for index, v := range values {
|
||||
seriesPainter.OverrideDrawingStyle(Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: theme.GetSeriesColor(index),
|
||||
FillColor: theme.GetSeriesColor(index),
|
||||
})
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
prevEndX = endx
|
||||
prevEndY = endy
|
||||
|
||||
seriesPainter.MoveTo(startx, starty)
|
||||
seriesPainter.LineTo(endx, endy)
|
||||
offset := labelLineWidth
|
||||
if endx < cx {
|
||||
offset *= -1
|
||||
}
|
||||
seriesPainter.MoveTo(endx, endy)
|
||||
endx += offset
|
||||
seriesPainter.LineTo(endx, endy)
|
||||
seriesPainter.Stroke()
|
||||
|
||||
textStyle := Style{
|
||||
FontColor: theme.GetTextColor(),
|
||||
FontSize: labelFontSize,
|
||||
Font: opt.Font,
|
||||
}
|
||||
if !series.Label.Color.IsZero() {
|
||||
textStyle.FontColor = 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)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
prevY = s.calculateY(prevY)
|
||||
if prevY > maxY {
|
||||
maxY = prevY
|
||||
}
|
||||
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 !s.series.Label.Color.IsZero() {
|
||||
textStyle.FontColor = s.series.Label.Color
|
||||
}
|
||||
seriesPainter.OverrideTextStyle(textStyle)
|
||||
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
|
|
@ -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 {
|
||||
|
|
@ -45,6 +46,7 @@ type RadarIndicator struct {
|
|||
}
|
||||
|
||||
type RadarChartOption struct {
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The font size
|
||||
Font *truetype.Font
|
||||
|
|
@ -62,6 +64,7 @@ type RadarChartOption struct {
|
|||
backgroundIsFilled bool
|
||||
}
|
||||
|
||||
// NewRadarIndicators returns a radar indicator list
|
||||
func NewRadarIndicators(names []string, values []float64) []RadarIndicator {
|
||||
if len(names) != len(values) {
|
||||
return nil
|
||||
|
|
@ -76,6 +79,7 @@ func NewRadarIndicators(names []string, values []float64) []RadarIndicator {
|
|||
return indicators
|
||||
}
|
||||
|
||||
// NewRadarChart returns a radar chart renderer
|
||||
func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
|
|
@ -197,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)
|
||||
|
|
@ -223,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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
39
range.go
39
range.go
|
|
@ -29,6 +29,7 @@ import (
|
|||
const defaultAxisDivideCount = 6
|
||||
|
||||
type axisRange struct {
|
||||
p *Painter
|
||||
divideCount int
|
||||
min float64
|
||||
max float64
|
||||
|
|
@ -37,13 +38,20 @@ type axisRange struct {
|
|||
}
|
||||
|
||||
type AxisRangeOption struct {
|
||||
Min float64
|
||||
Max float64
|
||||
Size int
|
||||
Boundary bool
|
||||
Painter *Painter
|
||||
// The min value of axis
|
||||
Min float64
|
||||
// The max value of axis
|
||||
Max float64
|
||||
// The size of axis
|
||||
Size int
|
||||
// Boundary gap
|
||||
Boundary bool
|
||||
// The count of divide
|
||||
DivideCount int
|
||||
}
|
||||
|
||||
// NewRange returns a axis range
|
||||
func NewRange(opt AxisRangeOption) axisRange {
|
||||
max := opt.Max
|
||||
min := opt.Min
|
||||
|
|
@ -54,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
|
||||
}
|
||||
|
|
@ -79,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,
|
||||
|
|
@ -88,18 +104,26 @@ func NewRange(opt AxisRangeOption) axisRange {
|
|||
}
|
||||
}
|
||||
|
||||
// Values returns values of range
|
||||
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))
|
||||
}
|
||||
|
|
@ -108,10 +132,13 @@ func (r *axisRange) getRestHeight(value float64) int {
|
|||
return r.size - r.getHeight(value)
|
||||
}
|
||||
|
||||
// GetRange returns a range of index
|
||||
func (r *axisRange) GetRange(index int) (float64, float64) {
|
||||
unit := float64(r.size) / float64(r.divideCount)
|
||||
return unit * float64(index), unit * float64(index+1)
|
||||
}
|
||||
|
||||
// AutoDivide divides the axis
|
||||
func (r *axisRange) AutoDivide() []int {
|
||||
return autoDivide(r.size, r.divideCount)
|
||||
}
|
||||
|
|
|
|||
49
series.go
49
series.go
|
|
@ -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 {
|
||||
|
|
@ -36,6 +36,7 @@ type SeriesData struct {
|
|||
Style Style
|
||||
}
|
||||
|
||||
// NewSeriesListDataFromValues returns a series list
|
||||
func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList {
|
||||
seriesList := make(SeriesList, len(values))
|
||||
for index, value := range values {
|
||||
|
|
@ -44,6 +45,7 @@ func NewSeriesListDataFromValues(values [][]float64, chartType ...string) Series
|
|||
return seriesList
|
||||
}
|
||||
|
||||
// NewSeriesFromValues returns a series
|
||||
func NewSeriesFromValues(values []float64, chartType ...string) Series {
|
||||
s := Series{
|
||||
Data: NewSeriesDataFromValues(values),
|
||||
|
|
@ -54,6 +56,7 @@ func NewSeriesFromValues(values []float64, chartType ...string) Series {
|
|||
return s
|
||||
}
|
||||
|
||||
// NewSeriesDataFromValues return a series data
|
||||
func NewSeriesDataFromValues(values []float64) []SeriesData {
|
||||
data := make([]SeriesData, len(values))
|
||||
for index, value := range values {
|
||||
|
|
@ -76,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 (
|
||||
|
|
@ -117,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
|
||||
|
|
@ -129,6 +140,9 @@ type Series struct {
|
|||
type SeriesList []Series
|
||||
|
||||
func (sl SeriesList) init() {
|
||||
if len(sl) == 0 {
|
||||
return
|
||||
}
|
||||
if sl[len(sl)-1].index != 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -159,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
|
||||
}
|
||||
|
|
@ -204,13 +222,19 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
|
|||
}
|
||||
|
||||
type seriesSummary struct {
|
||||
MaxIndex int
|
||||
MaxValue float64
|
||||
MinIndex int
|
||||
MinValue float64
|
||||
// The index of max value
|
||||
MaxIndex int
|
||||
// The max value
|
||||
MaxValue float64
|
||||
// The index of min value
|
||||
MinIndex int
|
||||
// The min value
|
||||
MinValue float64
|
||||
// THe average value
|
||||
AverageValue float64
|
||||
}
|
||||
|
||||
// Summary get summary of series
|
||||
func (s *Series) Summary() seriesSummary {
|
||||
minIndex := -1
|
||||
maxIndex := -1
|
||||
|
|
@ -237,6 +261,7 @@ func (s *Series) Summary() seriesSummary {
|
|||
}
|
||||
}
|
||||
|
||||
// Names returns the names of series list
|
||||
func (sl SeriesList) Names() []string {
|
||||
names := make([]string, len(sl))
|
||||
for index, s := range sl {
|
||||
|
|
@ -245,8 +270,10 @@ func (sl SeriesList) Names() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
// LabelFormatter label formatter
|
||||
type LabelFormatter func(index int, value float64, percent float64) string
|
||||
|
||||
// NewPieLabelFormatter returns a pie label formatter
|
||||
func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||
if len(layout) == 0 {
|
||||
layout = "{b}: {d}"
|
||||
|
|
@ -254,13 +281,23 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
|||
return NewLabelFormatter(seriesNames, layout)
|
||||
}
|
||||
|
||||
func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter {
|
||||
// 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 {
|
||||
layout = "{c}"
|
||||
}
|
||||
return NewLabelFormatter(seriesNames, layout)
|
||||
}
|
||||
|
||||
// NewLabelFormatter returns a label formaatter
|
||||
func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||
return func(index int, value, percent float64) string {
|
||||
// 如果无percent的则设置为<0
|
||||
|
|
|
|||
148
series_label.go
Normal file
148
series_label.go
Normal 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: ¶ms.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
|
||||
}
|
||||
89
series_test.go
Normal file
89
series_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSeriesListDataFromValues(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.Equal(SeriesList{
|
||||
{
|
||||
Type: ChartTypeBar,
|
||||
Data: []SeriesData{
|
||||
{
|
||||
Value: 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, NewSeriesListDataFromValues([][]float64{
|
||||
{
|
||||
1,
|
||||
},
|
||||
}, ChartTypeBar))
|
||||
}
|
||||
|
||||
func TestSeriesLists(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
seriesList := NewSeriesListDataFromValues([][]float64{
|
||||
{
|
||||
1,
|
||||
2,
|
||||
},
|
||||
{
|
||||
10,
|
||||
},
|
||||
}, ChartTypeBar)
|
||||
|
||||
assert.Equal(2, len(seriesList.Filter(ChartTypeBar)))
|
||||
assert.Equal(0, len(seriesList.Filter(ChartTypeLine)))
|
||||
|
||||
max, min := seriesList.GetMaxMin(0)
|
||||
assert.Equal(float64(10), max)
|
||||
assert.Equal(float64(1), min)
|
||||
|
||||
assert.Equal(seriesSummary{
|
||||
MaxIndex: 1,
|
||||
MaxValue: 2,
|
||||
MinIndex: 0,
|
||||
MinValue: 1,
|
||||
AverageValue: 1.5,
|
||||
}, seriesList[0].Summary())
|
||||
}
|
||||
|
||||
func TestFormatter(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.Equal("a: 12%", NewPieLabelFormatter([]string{
|
||||
"a",
|
||||
"b",
|
||||
}, "")(0, 10, 0.12))
|
||||
|
||||
assert.Equal("10", NewValueLabelFormatter([]string{
|
||||
"a",
|
||||
"b",
|
||||
}, "")(0, 10, 0.12))
|
||||
}
|
||||
254
start_zh.md
Normal file
254
start_zh.md
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
```
|
||||
438
table.go
Normal file
438
table.go
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"git.smarteching.com/zeni/go-chart/v2"
|
||||
"git.smarteching.com/zeni/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
type tableChart struct {
|
||||
p *Painter
|
||||
opt *TableChartOption
|
||||
}
|
||||
|
||||
// NewTableChart returns a table chart render
|
||||
func NewTableChart(p *Painter, opt TableChartOption) *tableChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = defaultTheme
|
||||
}
|
||||
return &tableChart{
|
||||
p: p,
|
||||
opt: &opt,
|
||||
}
|
||||
}
|
||||
|
||||
type TableCell struct {
|
||||
// Text the text of table cell
|
||||
Text string
|
||||
// Style the current style of table cell
|
||||
Style Style
|
||||
// Row the row index of table cell
|
||||
Row int
|
||||
// Column the column index of table cell
|
||||
Column int
|
||||
}
|
||||
|
||||
type TableChartOption struct {
|
||||
// The output type
|
||||
Type string
|
||||
// The width of table
|
||||
Width int
|
||||
// The theme
|
||||
Theme ColorPalette
|
||||
// The padding of table cell
|
||||
Padding Box
|
||||
// The header data of table
|
||||
Header []string
|
||||
// The data of table
|
||||
Data [][]string
|
||||
// The span list of table column
|
||||
Spans []int
|
||||
// The text align list of table cell
|
||||
TextAligns []string
|
||||
// The font size of table
|
||||
FontSize float64
|
||||
// The font family, which should be installed first
|
||||
FontFamily string
|
||||
Font *truetype.Font
|
||||
// The font color of table
|
||||
FontColor Color
|
||||
// The background color of header
|
||||
HeaderBackgroundColor Color
|
||||
// The header font color
|
||||
HeaderFontColor Color
|
||||
// The background color of row
|
||||
RowBackgroundColors []Color
|
||||
// The background color
|
||||
BackgroundColor Color
|
||||
// CellTextStyle customize text style of table cell
|
||||
CellTextStyle func(TableCell) *Style
|
||||
// CellStyle customize drawing style of table cell
|
||||
CellStyle func(TableCell) *Style
|
||||
}
|
||||
|
||||
type TableSetting struct {
|
||||
// The color of header
|
||||
HeaderColor Color
|
||||
// The color of heder text
|
||||
HeaderFontColor Color
|
||||
// The color of table text
|
||||
FontColor Color
|
||||
// The color list of row
|
||||
RowColors []Color
|
||||
// The padding of cell
|
||||
Padding Box
|
||||
}
|
||||
|
||||
var TableLightThemeSetting = TableSetting{
|
||||
HeaderColor: Color{
|
||||
R: 240,
|
||||
G: 240,
|
||||
B: 240,
|
||||
A: 255,
|
||||
},
|
||||
HeaderFontColor: Color{
|
||||
R: 98,
|
||||
G: 105,
|
||||
B: 118,
|
||||
A: 255,
|
||||
},
|
||||
FontColor: Color{
|
||||
R: 70,
|
||||
G: 70,
|
||||
B: 70,
|
||||
A: 255,
|
||||
},
|
||||
RowColors: []Color{
|
||||
drawing.ColorWhite,
|
||||
{
|
||||
R: 247,
|
||||
G: 247,
|
||||
B: 247,
|
||||
A: 255,
|
||||
},
|
||||
},
|
||||
Padding: Box{
|
||||
Left: 10,
|
||||
Top: 10,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
},
|
||||
}
|
||||
|
||||
var TableDarkThemeSetting = TableSetting{
|
||||
HeaderColor: Color{
|
||||
R: 38,
|
||||
G: 38,
|
||||
B: 42,
|
||||
A: 255,
|
||||
},
|
||||
HeaderFontColor: Color{
|
||||
R: 216,
|
||||
G: 217,
|
||||
B: 218,
|
||||
A: 255,
|
||||
},
|
||||
FontColor: Color{
|
||||
R: 216,
|
||||
G: 217,
|
||||
B: 218,
|
||||
A: 255,
|
||||
},
|
||||
RowColors: []Color{
|
||||
{
|
||||
R: 24,
|
||||
G: 24,
|
||||
B: 28,
|
||||
A: 255,
|
||||
},
|
||||
{
|
||||
R: 38,
|
||||
G: 38,
|
||||
B: 42,
|
||||
A: 255,
|
||||
},
|
||||
},
|
||||
Padding: Box{
|
||||
Left: 10,
|
||||
Top: 10,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
},
|
||||
}
|
||||
|
||||
var tableDefaultSetting = TableLightThemeSetting
|
||||
|
||||
// SetDefaultTableSetting sets the default setting for table
|
||||
func SetDefaultTableSetting(setting TableSetting) {
|
||||
tableDefaultSetting = setting
|
||||
}
|
||||
|
||||
type renderInfo struct {
|
||||
Width int
|
||||
Height int
|
||||
HeaderHeight int
|
||||
RowHeights []int
|
||||
ColumnWidths []int
|
||||
}
|
||||
|
||||
func (t *tableChart) render() (*renderInfo, error) {
|
||||
info := renderInfo{
|
||||
RowHeights: make([]int, 0),
|
||||
}
|
||||
p := t.p
|
||||
opt := t.opt
|
||||
if len(opt.Header) == 0 {
|
||||
return nil, errors.New("header can not be nil")
|
||||
}
|
||||
theme := opt.Theme
|
||||
if theme == nil {
|
||||
theme = p.theme
|
||||
}
|
||||
fontSize := opt.FontSize
|
||||
if fontSize == 0 {
|
||||
fontSize = 12
|
||||
}
|
||||
fontColor := opt.FontColor
|
||||
if fontColor.IsZero() {
|
||||
fontColor = tableDefaultSetting.FontColor
|
||||
}
|
||||
font := opt.Font
|
||||
if font == nil {
|
||||
font = theme.GetFont()
|
||||
}
|
||||
headerFontColor := opt.HeaderFontColor
|
||||
if opt.HeaderFontColor.IsZero() {
|
||||
headerFontColor = tableDefaultSetting.HeaderFontColor
|
||||
}
|
||||
|
||||
spans := opt.Spans
|
||||
if len(spans) != len(opt.Header) {
|
||||
newSpans := make([]int, len(opt.Header))
|
||||
for index := range opt.Header {
|
||||
if index >= len(spans) {
|
||||
newSpans[index] = 1
|
||||
} else {
|
||||
newSpans[index] = spans[index]
|
||||
}
|
||||
}
|
||||
spans = newSpans
|
||||
}
|
||||
|
||||
sum := sumInt(spans)
|
||||
values := autoDivideSpans(p.Width(), sum, spans)
|
||||
columnWidths := make([]int, 0)
|
||||
for index, v := range values {
|
||||
if index == len(values)-1 {
|
||||
break
|
||||
}
|
||||
columnWidths = append(columnWidths, values[index+1]-v)
|
||||
}
|
||||
info.ColumnWidths = columnWidths
|
||||
|
||||
height := 0
|
||||
textStyle := Style{
|
||||
FontSize: fontSize,
|
||||
FontColor: headerFontColor,
|
||||
FillColor: headerFontColor,
|
||||
Font: font,
|
||||
}
|
||||
|
||||
headerHeight := 0
|
||||
padding := opt.Padding
|
||||
if padding.IsZero() {
|
||||
padding = tableDefaultSetting.Padding
|
||||
}
|
||||
getCellTextStyle := opt.CellTextStyle
|
||||
if getCellTextStyle == nil {
|
||||
getCellTextStyle = func(_ TableCell) *Style {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// textAligns := opt.TextAligns
|
||||
getTextAlign := func(index int) string {
|
||||
if len(opt.TextAligns) <= index {
|
||||
return ""
|
||||
}
|
||||
return opt.TextAligns[index]
|
||||
}
|
||||
|
||||
// 表格单元的处理
|
||||
renderTableCells := func(
|
||||
currentStyle Style,
|
||||
rowIndex int,
|
||||
textList []string,
|
||||
currentHeight int,
|
||||
cellPadding Box,
|
||||
) int {
|
||||
cellMaxHeight := 0
|
||||
paddingHeight := cellPadding.Top + cellPadding.Bottom
|
||||
paddingWidth := cellPadding.Left + cellPadding.Right
|
||||
for index, text := range textList {
|
||||
cellStyle := getCellTextStyle(TableCell{
|
||||
Text: text,
|
||||
Row: rowIndex,
|
||||
Column: index,
|
||||
Style: currentStyle,
|
||||
})
|
||||
if cellStyle == nil {
|
||||
cellStyle = ¤tStyle
|
||||
}
|
||||
p.SetStyle(*cellStyle)
|
||||
x := values[index]
|
||||
y := currentHeight + cellPadding.Top
|
||||
width := values[index+1] - x
|
||||
x += cellPadding.Left
|
||||
width -= paddingWidth
|
||||
box := p.TextFit(text, x, y+int(fontSize), width, getTextAlign(index))
|
||||
// 计算最高的高度
|
||||
if box.Height()+paddingHeight > cellMaxHeight {
|
||||
cellMaxHeight = box.Height() + paddingHeight
|
||||
}
|
||||
}
|
||||
return cellMaxHeight
|
||||
}
|
||||
|
||||
// 表头的处理
|
||||
headerHeight = renderTableCells(textStyle, 0, opt.Header, height, padding)
|
||||
height += headerHeight
|
||||
info.HeaderHeight = headerHeight
|
||||
|
||||
// 表格内容的处理
|
||||
textStyle.FontColor = fontColor
|
||||
textStyle.FillColor = fontColor
|
||||
for index, textList := range opt.Data {
|
||||
cellHeight := renderTableCells(textStyle, index+1, textList, height, padding)
|
||||
info.RowHeights = append(info.RowHeights, cellHeight)
|
||||
height += cellHeight
|
||||
}
|
||||
|
||||
info.Width = p.Width()
|
||||
info.Height = height
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
|
||||
p := t.p
|
||||
opt := t.opt
|
||||
if !opt.BackgroundColor.IsZero() {
|
||||
p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
|
||||
}
|
||||
headerBGColor := opt.HeaderBackgroundColor
|
||||
if headerBGColor.IsZero() {
|
||||
headerBGColor = tableDefaultSetting.HeaderColor
|
||||
}
|
||||
|
||||
// 如果设置表头背景色
|
||||
p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true)
|
||||
currentHeight := info.HeaderHeight
|
||||
rowColors := opt.RowBackgroundColors
|
||||
if rowColors == nil {
|
||||
rowColors = tableDefaultSetting.RowColors
|
||||
}
|
||||
for index, h := range info.RowHeights {
|
||||
color := rowColors[index%len(rowColors)]
|
||||
child := p.Child(PainterPaddingOption(Box{
|
||||
Top: currentHeight,
|
||||
}))
|
||||
child.SetBackground(p.Width(), h, color, true)
|
||||
currentHeight += h
|
||||
}
|
||||
// 根据是否有设置表格样式调整背景色
|
||||
getCellStyle := opt.CellStyle
|
||||
if getCellStyle != nil {
|
||||
arr := [][]string{
|
||||
opt.Header,
|
||||
}
|
||||
arr = append(arr, opt.Data...)
|
||||
top := 0
|
||||
heights := []int{
|
||||
info.HeaderHeight,
|
||||
}
|
||||
heights = append(heights, info.RowHeights...)
|
||||
// 循环所有表格单元,生成背景色
|
||||
for i, textList := range arr {
|
||||
left := 0
|
||||
for j, v := range textList {
|
||||
style := getCellStyle(TableCell{
|
||||
Text: v,
|
||||
Row: i,
|
||||
Column: j,
|
||||
})
|
||||
if style != nil && !style.FillColor.IsZero() {
|
||||
padding := style.Padding
|
||||
child := p.Child(PainterPaddingOption(Box{
|
||||
Top: top + padding.Top,
|
||||
Left: left + padding.Left,
|
||||
}))
|
||||
w := info.ColumnWidths[j] - padding.Left - padding.Top
|
||||
h := heights[i] - padding.Top - padding.Bottom
|
||||
child.SetBackground(w, h, style.FillColor, true)
|
||||
}
|
||||
left += info.ColumnWidths[j]
|
||||
}
|
||||
top += heights[i]
|
||||
}
|
||||
}
|
||||
_, err := t.render()
|
||||
if err != nil {
|
||||
return BoxZero, err
|
||||
}
|
||||
|
||||
return Box{
|
||||
Right: info.Width,
|
||||
Bottom: info.Height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *tableChart) Render() (Box, error) {
|
||||
p := t.p
|
||||
opt := t.opt
|
||||
if !opt.BackgroundColor.IsZero() {
|
||||
p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
|
||||
}
|
||||
if opt.Font == nil && opt.FontFamily != "" {
|
||||
opt.Font, _ = GetFont(opt.FontFamily)
|
||||
}
|
||||
|
||||
r := p.render
|
||||
fn := chart.PNG
|
||||
if p.outputType == ChartOutputSVG {
|
||||
fn = chart.SVG
|
||||
}
|
||||
newRender, err := fn(p.Width(), 100)
|
||||
if err != nil {
|
||||
return BoxZero, err
|
||||
}
|
||||
p.render = newRender
|
||||
info, err := t.render()
|
||||
if err != nil {
|
||||
return BoxZero, err
|
||||
}
|
||||
p.render = r
|
||||
return t.renderWithInfo(info)
|
||||
}
|
||||
140
table_test.go
Normal file
140
table_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTableChart(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tests := []struct {
|
||||
render func(*Painter) ([]byte, error)
|
||||
result string
|
||||
}{
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewTableChart(p, TableChartOption{
|
||||
Header: []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
},
|
||||
Spans: []int{
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
// span和header不匹配,最后自动设置为1
|
||||
// 1,
|
||||
},
|
||||
Data: [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
},
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 0 0\nL 600 0\nL 600 35\nL 0 35\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(240,240,240,1.0)\"/><path d=\"M 0 35\nL 600 35\nL 600 90\nL 0 90\nL 0 35\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 0 90\nL 600 90\nL 600 125\nL 0 125\nL 0 90\" style=\"stroke-width:0;stroke:none;fill:rgba(247,247,247,1.0)\"/><path d=\"M 0 125\nL 600 125\nL 600 180\nL 0 180\nL 0 125\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><text x=\"10\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Name</text><text x=\"110\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Age</text><text x=\"210\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Address</text><text x=\"410\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tag</text><text x=\"510\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Action</text><text x=\"10\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">John</text><text x=\"10\" y=\"77\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Brown</text><text x=\"110\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">32</text><text x=\"210\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">New York No. 1 Lake Park</text><text x=\"410\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">nice,</text><text x=\"410\" y=\"77\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">developer</text><text x=\"510\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text><text x=\"10\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Jim Green</text><text x=\"110\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">42</text><text x=\"210\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">London No. 1 Lake Park</text><text x=\"410\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">wow</text><text x=\"510\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text><text x=\"10\" y=\"147\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Joe Black</text><text x=\"110\" y=\"147\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">32</text><text x=\"210\" y=\"147\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sidney No. 1 Lake Park</text><text x=\"410\" y=\"147\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">cool,</text><text x=\"410\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">teacher</text><text x=\"510\" y=\"147\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text></svg>",
|
||||
},
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewTableChart(p, TableChartOption{
|
||||
Header: []string{
|
||||
"Name",
|
||||
"Age",
|
||||
"Address",
|
||||
"Tag",
|
||||
"Action",
|
||||
},
|
||||
Data: [][]string{
|
||||
{
|
||||
"John Brown",
|
||||
"32",
|
||||
"New York No. 1 Lake Park",
|
||||
"nice, developer",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Jim Green ",
|
||||
"42",
|
||||
"London No. 1 Lake Park",
|
||||
"wow",
|
||||
"Send Mail",
|
||||
},
|
||||
{
|
||||
"Joe Black ",
|
||||
"32",
|
||||
"Sidney No. 1 Lake Park",
|
||||
"cool, teacher",
|
||||
"Send Mail",
|
||||
},
|
||||
},
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 0 0\nL 600 0\nL 600 35\nL 0 35\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(240,240,240,1.0)\"/><path d=\"M 0 35\nL 600 35\nL 600 90\nL 0 90\nL 0 35\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 0 90\nL 600 90\nL 600 145\nL 0 145\nL 0 90\" style=\"stroke-width:0;stroke:none;fill:rgba(247,247,247,1.0)\"/><path d=\"M 0 145\nL 600 145\nL 600 200\nL 0 200\nL 0 145\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><text x=\"10\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Name</text><text x=\"130\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Age</text><text x=\"250\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Address</text><text x=\"370\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Tag</text><text x=\"490\" y=\"22\" style=\"stroke-width:0;stroke:none;fill:rgba(98,105,118,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Action</text><text x=\"10\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">John Brown</text><text x=\"130\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">32</text><text x=\"250\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">New York No.</text><text x=\"250\" y=\"77\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1 Lake Park</text><text x=\"370\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">nice,</text><text x=\"370\" y=\"77\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">developer</text><text x=\"490\" y=\"57\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text><text x=\"10\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Jim Green</text><text x=\"130\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">42</text><text x=\"250\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">London No. 1</text><text x=\"250\" y=\"132\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Lake Park</text><text x=\"370\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">wow</text><text x=\"490\" y=\"112\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text><text x=\"10\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Joe Black</text><text x=\"130\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">32</text><text x=\"250\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Sidney No. 1</text><text x=\"250\" y=\"187\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Lake Park</text><text x=\"370\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">cool, teacher</text><text x=\"490\" y=\"167\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">Send Mail</text></svg>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
p, err := NewPainter(PainterOptions{
|
||||
Type: ChartOutputSVG,
|
||||
Width: 600,
|
||||
Height: 400,
|
||||
}, PainterThemeOption(defaultTheme))
|
||||
assert.Nil(err)
|
||||
data, err := tt.render(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(tt.result, string(data))
|
||||
}
|
||||
}
|
||||
58
theme.go
58
theme.go
|
|
@ -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"),
|
||||
|
|
@ -220,6 +239,7 @@ func init() {
|
|||
SetDefaultTheme(ThemeLight)
|
||||
}
|
||||
|
||||
// SetDefaultTheme sets default theme
|
||||
func SetDefaultTheme(name string) {
|
||||
defaultTheme = NewTheme(name)
|
||||
}
|
||||
|
|
@ -240,7 +260,8 @@ func NewTheme(name string) ColorPalette {
|
|||
if !ok {
|
||||
p = palettes[ThemeLight]
|
||||
}
|
||||
return p
|
||||
clone := *p
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (t *themeColorPalette) IsDark() bool {
|
||||
|
|
@ -251,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
|
||||
|
|
@ -275,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
|
||||
}
|
||||
|
|
|
|||
8
title.go
8
title.go
|
|
@ -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.
|
||||
|
|
@ -84,6 +80,7 @@ type titlePainter struct {
|
|||
opt *TitleOption
|
||||
}
|
||||
|
||||
// NewTitlePainter returns a title renderer
|
||||
func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter {
|
||||
return &titlePainter{
|
||||
p: p,
|
||||
|
|
@ -96,6 +93,9 @@ func (t *titlePainter) Render() (Box, error) {
|
|||
p := t.p
|
||||
theme := opt.Theme
|
||||
|
||||
if theme == nil {
|
||||
theme = p.theme
|
||||
}
|
||||
if opt.Text == "" && opt.Subtext == "" {
|
||||
return BoxZero, nil
|
||||
}
|
||||
|
|
|
|||
93
title_test.go
Normal file
93
title_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTitleRenderer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
tests := []struct {
|
||||
render func(*Painter) ([]byte, error)
|
||||
result string
|
||||
}{
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewTitlePainter(p, TitleOption{
|
||||
Text: "title",
|
||||
Subtext: "subTitle",
|
||||
Left: "20",
|
||||
Top: "20",
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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<text x=\"34\" y=\"35\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"20\" y=\"50\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">subTitle</text></svg>",
|
||||
},
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewTitlePainter(p, TitleOption{
|
||||
Text: "title",
|
||||
Subtext: "subTitle",
|
||||
Left: "20%",
|
||||
Top: "20",
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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<text x=\"134\" y=\"35\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"120\" y=\"50\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">subTitle</text></svg>",
|
||||
},
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
_, err := NewTitlePainter(p, TitleOption{
|
||||
Text: "title",
|
||||
Subtext: "subTitle",
|
||||
Left: PositionRight,
|
||||
}).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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<text x=\"558\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"544\" y=\"30\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">subTitle</text></svg>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
p, err := NewPainter(PainterOptions{
|
||||
Type: ChartOutputSVG,
|
||||
Width: 600,
|
||||
Height: 400,
|
||||
}, PainterThemeOption(defaultTheme))
|
||||
assert.Nil(err)
|
||||
data, err := tt.render(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(tt.result, string(data))
|
||||
}
|
||||
}
|
||||
57
util.go
57
util.go
|
|
@ -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 {
|
||||
|
|
@ -90,6 +90,30 @@ func autoDivide(max, size int) []int {
|
|||
return values
|
||||
}
|
||||
|
||||
func autoDivideSpans(max, size int, spans []int) []int {
|
||||
values := autoDivide(max, size)
|
||||
// 重新合并
|
||||
if len(spans) != 0 {
|
||||
newValues := make([]int, len(spans)+1)
|
||||
newValues[0] = 0
|
||||
end := 0
|
||||
for index, v := range spans {
|
||||
end += v
|
||||
newValues[index+1] = values[end]
|
||||
}
|
||||
values = newValues
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func sumInt(values []int) int {
|
||||
sum := 0
|
||||
for _, v := range values {
|
||||
sum += v
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
|
||||
func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) {
|
||||
maxWidth := 0
|
||||
|
|
@ -136,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)
|
||||
}
|
||||
|
|
@ -228,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
|
||||
}
|
||||
|
|
|
|||
36
util_test.go
36
util_test.go
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
13
xaxis.go
13
xaxis.go
|
|
@ -47,12 +47,19 @@ 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
|
||||
}
|
||||
|
||||
const defaultXAxisHeight = 30
|
||||
|
||||
// NewXAxisOption returns a x axis option
|
||||
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
|
||||
opt := XAxisOption{
|
||||
Data: data,
|
||||
|
|
@ -80,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
|
||||
|
|
@ -89,6 +99,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
|
|||
return axisOpt
|
||||
}
|
||||
|
||||
// NewBottomXAxis returns a bottom x axis renderer
|
||||
func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
|
||||
return NewAxisPainter(p, opt.ToAxisOption())
|
||||
}
|
||||
|
|
|
|||
25
yaxis.go
25
yaxis.go
|
|
@ -47,9 +47,14 @@ 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
|
||||
func NewYAxisOptions(data []string, others ...[]string) []YAxisOption {
|
||||
arr := [][]string{
|
||||
data,
|
||||
|
|
@ -64,14 +69,18 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption {
|
|||
return opts
|
||||
}
|
||||
|
||||
func (opt *YAxisOption) ToAxisOption() AxisOption {
|
||||
func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
|
||||
position := PositionLeft
|
||||
if opt.Position == PositionRight {
|
||||
position = PositionRight
|
||||
}
|
||||
theme := opt.Theme
|
||||
if theme == nil {
|
||||
theme = p.theme
|
||||
}
|
||||
axisOpt := AxisOption{
|
||||
Formatter: opt.Formatter,
|
||||
Theme: opt.Theme,
|
||||
Theme: theme,
|
||||
Data: opt.Data,
|
||||
Position: position,
|
||||
FontSize: opt.FontSize,
|
||||
|
|
@ -80,8 +89,9 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
|||
FontColor: opt.FontColor,
|
||||
BoundaryGap: FalseFlag(),
|
||||
SplitLineShow: true,
|
||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||
SplitLineColor: theme.GetAxisSplitLineColor(),
|
||||
Show: opt.Show,
|
||||
Unit: opt.Unit,
|
||||
}
|
||||
if !opt.Color.IsZero() {
|
||||
axisOpt.FontColor = opt.Color
|
||||
|
|
@ -92,21 +102,26 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
|||
axisOpt.StrokeWidth = 1
|
||||
axisOpt.SplitLineShow = false
|
||||
}
|
||||
if opt.SplitLineShow != nil {
|
||||
axisOpt.SplitLineShow = *opt.SplitLineShow
|
||||
}
|
||||
return axisOpt
|
||||
}
|
||||
|
||||
// NewLeftYAxis returns a left y axis renderer
|
||||
func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||
p = p.Child(PainterPaddingOption(Box{
|
||||
Bottom: defaultXAxisHeight,
|
||||
}))
|
||||
return NewAxisPainter(p, opt.ToAxisOption())
|
||||
return NewAxisPainter(p, opt.ToAxisOption(p))
|
||||
}
|
||||
|
||||
// NewRightYAxis returns a right y axis renderer
|
||||
func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||
p = p.Child(PainterPaddingOption(Box{
|
||||
Bottom: defaultXAxisHeight,
|
||||
}))
|
||||
axisOpt := opt.ToAxisOption()
|
||||
axisOpt := opt.ToAxisOption(p)
|
||||
axisOpt.Position = PositionRight
|
||||
axisOpt.SplitLineShow = false
|
||||
return NewAxisPainter(p, axisOpt)
|
||||
|
|
|
|||
70
yaxis_test.go
Normal file
70
yaxis_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRightYAxis(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
tests := []struct {
|
||||
render func(*Painter) ([]byte, error)
|
||||
result string
|
||||
}{
|
||||
{
|
||||
render: func(p *Painter) ([]byte, error) {
|
||||
opt := NewYAxisOptions([]string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
})[0]
|
||||
_, err := NewRightYAxis(p, opt).Render()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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<text x=\"581\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">a</text><text x=\"581\" y=\"133\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">b</text><text x=\"581\" y=\"250\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">c</text><text x=\"581\" y=\"367\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">d</text></svg>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
p, err := NewPainter(PainterOptions{
|
||||
Type: ChartOutputSVG,
|
||||
Width: 600,
|
||||
Height: 400,
|
||||
}, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{
|
||||
Top: 10,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
Left: 10,
|
||||
}))
|
||||
assert.Nil(err)
|
||||
data, err := tt.render(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(tt.result, string(data))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue