Compare commits

..

No commits in common. "main" and "v2.0.0-alpha" have entirely different histories.

64 changed files with 383 additions and 3880 deletions

View file

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

2
.gitignore vendored
View file

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

View file

@ -1,13 +1,11 @@
# go-charts
Clone from https://github.com/vicanso/go-charts
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions)
[中文](./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`. The default format is `png` and the default theme is `light`.
`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`.
`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`.
@ -17,13 +15,9 @@ 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`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`.
These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
## Example
@ -35,7 +29,7 @@ More examples can be found in the [./examples/](./examples/) directory.
package main
import (
charts "git.smarteching.com/zeni/go-charts/v2"
charts "github.com/vicanso/go-charts/v2"
)
func main() {
@ -101,7 +95,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -176,7 +170,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -233,7 +227,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -288,7 +282,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -346,7 +340,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -380,78 +374,13 @@ 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 (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {

View file

@ -3,7 +3,7 @@
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions)
`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg``png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`默认的输入格式为`png`,默认主题为`light`
`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg``png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`
`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg``png`)方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana)
@ -12,13 +12,9 @@
<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`, `horizontal bar`, `pie`, `radar`, `funnel` 以及 `table`
支持以下的图表类型:`line`, `bar`, `pie`, `radar` 以及 `funnel`
## 示例
@ -32,7 +28,7 @@
package main
import (
charts "git.smarteching.com/zeni/go-charts/v2"
charts "github.com/vicanso/go-charts/v2"
)
func main() {
@ -98,7 +94,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -173,7 +169,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -230,7 +226,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -285,7 +281,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -343,7 +339,7 @@ func main() {
package main
import (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -377,77 +373,13 @@ 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 (
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/go-charts/v2"
)
func main() {
@ -569,7 +501,7 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op
默认使用的字符为`roboto`为英文字体库,因此如果需要显示中文字符需要增加中文字体库,`InstallFont`函数可添加对应的字体库,成功添加之后则指定`title.textStyle.fontFamily`即可。
在浏览器中使用`svg`时,如果指定的`fontFamily`不支持中文字符,展示的中文并不会乱码,但是会导致在计算字符宽度等错误。
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败字体尽量选择Bold类型否则生成的图片会有点模糊
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。
示例见 [examples/chinese/main.go](examples/chinese/main.go)

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

54
axis.go
View file

@ -26,7 +26,7 @@ import (
"strings"
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type axisPainter struct {
@ -63,8 +63,6 @@ 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
@ -77,11 +75,6 @@ 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) {
@ -159,31 +152,9 @@ func (a *axisPainter) Render() (Box, error) {
}
top.SetDrawingStyle(style).OverrideTextStyle(style)
isTextRotation := opt.TextRotation != 0
if isTextRotation {
top.SetTextRotation(opt.TextRotation)
}
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
if isTextRotation {
top.ClearTextRotation()
}
// 增加30px来计算文本展示区域
textFillWidth := float64(textMaxWidth + 20)
// 根据文本宽度计算较为符合的展示项
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++
}
}
textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth))
unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber)))
width := 0
height := 0
@ -257,7 +228,6 @@ func (a *axisPainter) Render() (Box, error) {
Length: tickLength,
Unit: unit,
Orient: orient,
First: opt.FirstAxis,
})
p.LineStroke([]Point{
{
@ -276,19 +246,15 @@ func (a *axisPainter) Render() (Box, error) {
Top: labelPaddingTop,
Right: labelPaddingRight,
})).MultiText(MultiTextOption{
First: opt.FirstAxis,
Align: textAlign,
TextList: data,
Orient: orient,
Unit: unit,
Position: labelPosition,
TextRotation: opt.TextRotation,
Offset: opt.LabelOffset,
Align: textAlign,
TextList: data,
Orient: orient,
Unit: unit,
Position: labelPosition,
})
// 显示辅助线
if opt.SplitLineShow {
style.StrokeColor = opt.SplitLineColor
style.StrokeWidth = 1
top.OverrideDrawingStyle(style)
if isVertical {
x0 := p.Width()
@ -297,9 +263,7 @@ func (a *axisPainter) Render() (Box, error) {
x0 = 0
x1 = top.Width() - p.Width()
}
yValues := autoDivide(height, tickCount)
yValues = yValues[0 : len(yValues)-1]
for _, y := range yValues {
for _, y := range autoDivide(height, tickCount) {
top.LineStroke([]Point{
{
X: x0,

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/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\"/></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\"/><path d=\"M 41 400\nL 600 400\" 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\"/></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\"/><path d=\"M 0 400\nL 559 400\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
},
// 顶部
{

View file

@ -23,10 +23,8 @@
package charts
import (
"math"
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type barChart struct {
@ -34,7 +32,6 @@ 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
@ -46,7 +43,6 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart {
}
type BarChartOption struct {
// The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@ -61,10 +57,14 @@ type BarChartOption struct {
// The option of title
Title TitleOption
// The legend option
Legend LegendOption
BarWidth int
// Margin of bar
BarMargin int
Legend LegendOption
}
type barChartLabelRenderOption struct {
Text string
Style Style
X int
Y int
}
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@ -73,7 +73,6 @@ 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(),
})
@ -90,17 +89,9 @@ 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)) / seriesCount
if opt.BarWidth > 0 && opt.BarWidth < barWidth {
barWidth = opt.BarWidth
// 重新计算margin
margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2
}
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
barMaxHeight := seriesPainter.Height()
theme := opt.Theme
seriesNames := seriesList.Names()
@ -111,6 +102,7 @@ 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]
@ -118,18 +110,6 @@ 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
@ -147,25 +127,14 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
}
top := barMaxHeight - h
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)
}
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
})
// 用于生成marker point
points[j] = Point{
// 居中的位置
@ -179,33 +148,30 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Y: top,
}
// 如果label不需要展示则返回
if labelPainter == nil {
if !series.Label.Show {
continue
}
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
}
}
distance := series.Label.Distance
if distance == 0 {
distance = 5
}
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,
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,
})
}
@ -224,6 +190,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Range: yRange,
})
}
for _, labelOpt := range labelRenderOptions {
seriesPainter.OverrideTextStyle(labelOpt.Style)
seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y)
}
// 最大、最小的mark point
err := doRender(rendererList...)
if err != nil {

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,7 @@ import (
"sort"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
)
type ChartOption struct {
@ -61,24 +62,8 @@ 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
@ -123,12 +108,9 @@ func TitleOptionFunc(title TitleOption) OptionFunc {
}
// TitleTextOptionFunc set title text of chart
func TitleTextOptionFunc(text string, subtext ...string) OptionFunc {
func TitleTextOptionFunc(text string) OptionFunc {
return func(opt *ChartOption) {
opt.Title.Text = text
if len(subtext) != 0 {
opt.Title.Subtext = subtext[0]
}
}
}
@ -273,10 +255,7 @@ func (o *ChartOption) fillDefault() {
o.font, _ = GetFont(o.FontFamily)
if o.font == nil {
o.font, _ = GetDefaultFont()
} else {
// 如果指定了字体,则设置主题的字体
t.SetFont(o.font)
o.font, _ = chart.GetDefaultFont()
}
if o.BackgroundColor.IsZero() {
o.BackgroundColor = t.GetBackgroundColor()
@ -357,70 +336,3 @@ 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

View file

@ -24,46 +24,27 @@ 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)
}
@ -125,16 +106,14 @@ 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
}
legendResult, err := NewLegendPainter(p, opt.LegendOption).Render()
_, err := NewLegendPainter(p, opt.LegendOption).Render()
if err != nil {
return nil, err
}
legendHeight = legendResult.Height()
}
// 如果有标题
@ -148,15 +127,9 @@ 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: top + 20,
Top: titleBox.Height() + 20,
}))
}
@ -186,26 +159,21 @@ 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{
Painter: p,
Min: min,
Max: max,
Min: min,
Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
DivideCount: divideCount,
DivideCount: defaultAxisDivideCount,
})
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 {
@ -215,16 +183,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
yAxisOption.Data = r.Values()
} else {
yAxisOption.isCategoryAxis = true
// 由于x轴为value部分因此计算其label单独处理
opt.XAxis.Data = NewRange(AxisRangeOption{
Painter: p,
Min: min,
Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
DivideCount: defaultAxisDivideCount,
}).Values()
opt.XAxis.Data = r.Values()
opt.XAxis.isValueAxis = true
}
reverseStringSlice(yAxisOption.Data)
@ -301,9 +260,6 @@ 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))
}
@ -346,8 +302,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
TitleOption: opt.Title,
LegendOption: opt.Legend,
axisReversed: axisReversed,
// 前置已设置背景色
backgroundIsFilled: true,
}
if isChild {
renderOpt.backgroundIsFilled = true
}
if len(pieSeriesList) != 0 ||
len(radarSeriesList) != 0 ||
@ -359,10 +316,6 @@ 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 {
@ -375,11 +328,9 @@ 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,
BarWidth: opt.BarWidth,
BarMargin: opt.BarMargin,
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
}).render(renderResult, barSeriesList)
return err
})
@ -391,8 +342,6 @@ 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
@ -414,13 +363,9 @@ 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,
SymbolShow: opt.SymbolShow,
StrokeWidth: opt.LineStrokeWidth,
FillArea: opt.FillArea,
Opacity: opt.Opacity,
Theme: opt.theme,
Font: opt.font,
XAxis: opt.XAxis,
}).render(renderResult, lineSeriesList)
return err
})

View file

@ -1,255 +0,0 @@
// 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"))
}
}
}

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -2,11 +2,10 @@ package main
import (
"bytes"
"fmt"
"net/http"
"strconv"
charts "git.smarteching.com/zeni/go-charts/v2"
charts "github.com/vicanso/go-charts/v2"
)
var html = `<!DOCTYPE html>
@ -93,48 +92,6 @@ 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)
@ -262,35 +219,6 @@ 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{
@ -355,10 +283,6 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Value: 180,
},
},
Label: charts.SeriesLabel{
Show: true,
Position: charts.PositionBottom,
},
},
},
},
@ -1969,6 +1893,5 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/echarts", echartsHandler)
fmt.Println("http://127.0.0.1:3012/")
http.ListenAndServe(":3012", nil)
}

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"git.smarteching.com/zeni/go-charts/v2"
"github.com/vicanso/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 = os.WriteFile(file, buf, 0600)
err = ioutil.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@ -29,8 +29,7 @@ func main() {
120,
132,
101,
// 134,
charts.GetNullValue(),
134,
90,
230,
210,
@ -90,23 +89,7 @@ func main() {
"Video Ads",
"Direct",
"Search Engine",
}, "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)
}
},
}, charts.PositionCenter),
)
if err != nil {

View file

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

View file

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

View file

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

View file

@ -1,178 +0,0 @@
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)
}
}

View file

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

21
font.go
View file

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

View file

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

View file

@ -23,6 +23,9 @@
package charts
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype"
)
@ -31,7 +34,6 @@ 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 {
@ -42,7 +44,6 @@ 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
@ -54,7 +55,6 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
}
type FunnelChartOption struct {
// The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@ -92,23 +92,13 @@ 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
// 最大最小值一致则为100%
widthPercent := 100.0
if offset != 0 {
widthPercent = (value - min) / offset
}
widthPercent := (value - min) / (max - min)
w := int(widthPercent * float64(width))
widthList[index] = w
// 如果最大值为0则占比100%
percent := 1.0
if max != 0 {
percent = value / max
}
textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent)
p := humanize.CommafWithDigits(value/max*100, 2) + "%"
textList[index] = fmt.Sprintf("%s(%s)", item.Name, p)
}
for index, w := range widthList {

12
go.mod
View file

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

22
go.sum
View file

@ -1,17 +1,23 @@
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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/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/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=
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=
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
View file

@ -28,27 +28,16 @@ type gridPainter struct {
}
type GridPainterOption struct {
// 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
StrokeWidth float64
StrokeColor Color
Column int
Row int
IgnoreFirstRow bool
IgnoreLastRow bool
IgnoreFirstColumn bool
// Ignore last column
IgnoreLastColumn bool
IgnoreLastColumn bool
}
// NewGridPainter returns new a grid renderer
func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
return &gridPainter{
p: p,
@ -83,7 +72,6 @@ func (g *gridPainter) Render() (Box, error) {
})
g.p.Grid(GridOption{
Column: opt.Column,
ColumnSpans: opt.ColumnSpans,
Row: opt.Row,
IgnoreColumnLines: ignoreColumnLines,
IgnoreRowLines: ignoreRowLines,

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2/drawing"
)
func TestGrid(t *testing.T) {
@ -54,24 +54,6 @@ 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{

View file

@ -24,7 +24,7 @@ package charts
import (
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type horizontalBarChart struct {
@ -33,7 +33,6 @@ type horizontalBarChart struct {
}
type HorizontalBarChartOption struct {
// The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@ -48,13 +47,9 @@ type HorizontalBarChartOption struct {
// The option of title
Title TitleOption
// The legend option
Legend LegendOption
BarHeight int
// Margin of bar
BarMargin int
Legend LegendOption
}
// NewHorizontalBarChart returns a horizontal bar chart renderer
func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
if opt.Theme == nil {
opt.Theme = defaultTheme
@ -83,46 +78,24 @@ 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)) / seriesCount
if opt.BarHeight > 0 && opt.BarHeight < barHeight {
barHeight = opt.BarHeight
margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2
}
barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
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
@ -141,57 +114,16 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
fillColor = item.Style.FillColor
}
right := w
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)
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).Rect(chart.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
})
}
}
err := doRender(rendererList...)
if err != nil {
return BoxZero, err
}
return p.box, nil
}

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,6 @@ const IconRect = "rect"
const IconLineDot = "lineDot"
type LegendOption struct {
// The theme
Theme ColorPalette
// Text array of legend
Data []string
@ -59,11 +58,8 @@ type LegendOption struct {
FontColor Color
// The flag for show legend, set this to *false will hide legend
Show *bool
// The padding of legend
Padding Box
}
// NewLegendOption returns a legend option
func NewLegendOption(labels []string, left ...string) LegendOption {
opt := LegendOption{
Data: labels,
@ -74,7 +70,6 @@ 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 {
@ -86,7 +81,6 @@ func (opt *LegendOption) IsEmpty() bool {
return isEmpty
}
// NewLegendPainter returns a legend renderer
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
return &legendPainter{
p: p,
@ -113,11 +107,9 @@ func (l *legendPainter) Render() (Box, error) {
if opt.Left == "" {
opt.Left = PositionCenter
}
padding := opt.Padding
if padding.IsZero() {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
p := l.p.Child(PainterPaddingOption(Box{
Top: 5,
}))
p.SetTextStyle(Style{
FontSize: opt.FontSize,
FontColor: opt.FontColor,
@ -139,19 +131,13 @@ 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)
@ -180,13 +166,8 @@ 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
@ -208,22 +189,12 @@ 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
@ -232,7 +203,7 @@ func (l *legendPainter) Render() (Box, error) {
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
x0 = drawIcon(y0, x0)
x0 = drawIcon(0, x0)
}
if opt.Orient == OrientVertical {
y0 += offset
@ -241,11 +212,10 @@ func (l *legendPainter) Render() (Box, error) {
x0 += offset
y0 = y
}
height = y0 - startY + 10
}
return Box{
Right: width,
Bottom: height + padding.Bottom + padding.Top,
Bottom: height,
}, nil
}

View file

@ -23,10 +23,8 @@
package charts
import (
"math"
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2/drawing"
)
type lineChart struct {
@ -34,7 +32,6 @@ 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
@ -46,7 +43,6 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
}
type LineChartOption struct {
// The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@ -62,16 +58,8 @@ 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) {
@ -103,82 +91,25 @@ 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: strokeWidth,
}
if len(series.Style.StrokeDashArray) > 0 {
drawingStyle.StrokeDashArray = series.Style.StrokeDashArray
StrokeWidth: defaultStrokeWidth,
}
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)
@ -190,9 +121,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
}
drawingStyle.StrokeWidth = 1
seriesPainter.SetDrawingStyle(drawingStyle)
if !isFalse(opt.SymbolShow) {
seriesPainter.Dots(points)
}
seriesPainter.Dots(points)
markPointPainter.Add(markPointRenderOption{
FillColor: seriesColor,
Font: opt.Font,

File diff suppressed because one or more lines are too long

View file

@ -24,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,7 +48,6 @@ 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,
@ -74,7 +73,7 @@ func (m *markLinePainter) Render() (Box, error) {
}
font := opt.Font
if font == nil {
font, _ = GetDefaultFont()
font, _ = chart.GetDefaultFont()
}
summary := s.Summary()
for _, markLine := range s.MarkLine.Data {

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2/drawing"
)
func TestMarkLine(t *testing.T) {
@ -55,7 +55,6 @@ func TestMarkLine(t *testing.T) {
StrokeColor: drawing.ColorBlack,
Series: series,
Range: NewRange(AxisRangeOption{
Painter: p,
Min: 0,
Max: 5,
Size: p.Height(),
@ -68,7 +67,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=\"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>",
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>",
},
}
for _, tt := range tests {

View file

@ -26,7 +26,6 @@ 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 {
@ -55,7 +54,6 @@ type markPointRenderOption struct {
Points []Point
}
// NewMarkPointPainter returns a mark point renderer
func NewMarkPointPainter(p *Painter) *markPointPainter {
return &markPointPainter{
p: p,
@ -65,6 +63,7 @@ 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 {
@ -76,22 +75,15 @@ func (m *markPointPainter) Render() (Box, error) {
if symbolSize == 0 {
symbolSize = 30
}
textStyle := Style{
painter.OverrideDrawingStyle(Style{
FillColor: opt.FillColor,
}).OverrideTextStyle(Style{
FontColor: theme.GetTextColor(),
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 {
@ -103,11 +95,6 @@ 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)
}
}

View file

@ -26,7 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/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(238,238,238,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(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text></svg>",
},
}

View file

@ -28,11 +28,9 @@ import (
"math"
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type ValueFormatter func(float64) string
type Painter struct {
render chart.Renderer
box Box
@ -40,9 +38,6 @@ type Painter struct {
parent *Painter
style Style
theme ColorPalette
// 类型
outputType string
valueFormatter ValueFormatter
}
type PainterOptions struct {
@ -59,8 +54,6 @@ type PainterOptions struct {
type PainterOption func(*Painter)
type TicksOption struct {
// the first tick
First int
Length int
Orient string
Count int
@ -73,17 +66,11 @@ type MultiTextOption struct {
Unit int
Position string
Align string
// The text rotation of label
TextRotation float64
Offset Box
// The first text index
First int
}
type GridOption struct {
Column int
Row int
ColumnSpans []int
Column int
Row int
// 忽略不展示的column
IgnoreColumnLines []int
// 忽略不展示的row
@ -149,14 +136,14 @@ func PainterWidthHeightOption(width, height int) PainterOption {
}
}
// NewPainter creates a painter
// NewPainter creates a new 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 := GetDefaultFont()
f, err := chart.GetDefaultFont()
if err != nil {
return nil, err
}
@ -181,8 +168,6 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
Bottom: opts.Height,
},
font: font,
// 类型
outputType: opts.Type,
}
p.setOptions(opt...)
if p.theme == nil {
@ -198,9 +183,6 @@ 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,
@ -451,18 +433,11 @@ 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 y == int(math.MaxInt32) {
p.Stroke()
shouldMoveTo = true
continue
}
if shouldMoveTo || index == 0 {
if index == 0 {
p.MoveTo(x, y)
shouldMoveTo = false
} else {
p.LineTo(x, y)
}
@ -492,7 +467,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter {
return p
}
func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter {
func (p *Painter) SetBackground(width, height int, color Color) *Painter {
r := p.render
s := chart.Style{
FillColor: color,
@ -500,20 +475,12 @@ func (p *Painter) SetBackground(width, height int, color Color, inside ...bool)
// 背景色
p.SetDrawingStyle(s)
defer p.ResetStyle()
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)
}
// 设置背景色不使用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
}
@ -565,20 +532,7 @@ func (p *Painter) Text(body string, x, y int) *Painter {
return p
}
func (p *Painter) TextRotation(body string, x, y int, radians float64) {
p.render.SetTextRotation(radians)
p.render.Text(body, x+p.box.Left, y+p.box.Top)
p.render.ClearTextRotation()
}
func (p *Painter) SetTextRotation(radians float64) {
p.render.SetTextRotation(radians)
}
func (p *Painter) ClearTextRotation() {
p.render.ClearTextRotation()
}
func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box {
func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
style := p.style
textWarp := style.TextWrap
style.TextWrap = chart.TextWrapWord
@ -587,24 +541,11 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch
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()
lineBox := r.MeasureText(line)
switch textAlign {
case AlignRight:
x0 += width - lineBox.Width()
case AlignCenter:
x0 += (width - lineBox.Width()) >> 1
}
p.Text(line, x0, y0)
lineBox := r.MeasureText(line)
output.Right = chart.MaxInt(lineBox.Right, output.Right)
output.Bottom += lineBox.Height()
if index < len(lines)-1 {
@ -620,7 +561,6 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
return p
}
count := opt.Count
first := opt.First
width := p.Width()
height := p.Height()
unit := 1
@ -635,10 +575,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
values = autoDivide(width, count)
}
for index, value := range values {
if index < first {
continue
}
if (index-first)%unit != 0 {
if index%unit != 0 {
continue
}
if isVertical {
@ -674,15 +611,12 @@ 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()
@ -693,19 +627,10 @@ 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 index < opt.First {
if opt.Unit != 0 && index%opt.Unit != 0 {
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 {
@ -726,13 +651,8 @@ 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
}
@ -770,12 +690,8 @@ func (p *Painter) Grid(opt GridOption) *Painter {
})
}
}
columnCount := sumInt(opt.ColumnSpans)
if columnCount == 0 {
columnCount = opt.Column
}
if columnCount > 0 {
values := autoDivideSpans(width, columnCount, opt.ColumnSpans)
if opt.Column > 0 {
values := autoDivide(width, opt.Column)
drawLines(values, opt.IgnoreColumnLines, true)
}
if opt.Row > 0 {
@ -803,48 +719,6 @@ 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()
@ -860,7 +734,3 @@ func (p *Painter) LegendLineDot(box Box) *Painter {
p.FillStroke()
return p
}
func (p *Painter) GetRenderer() chart.Renderer {
return p.render
}

View file

@ -28,8 +28,8 @@ import (
"github.com/golang/freetype/truetype"
"github.com/stretchr/testify/assert"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
FillColor: Color{
FillColor: drawing.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: Color{
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
@ -297,7 +297,7 @@ func TestPainter(t *testing.T) {
{
fn: func(p *Painter) {
p.SetDrawingStyle(Style{
FillColor: Color{
FillColor: drawing.Color{
R: 84,
G: 112,
B: 198,
@ -343,29 +343,6 @@ 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{
@ -374,7 +351,7 @@ func TestPainterTextFit(t *testing.T) {
Type: ChartOutputSVG,
})
assert.Nil(err)
f, _ := GetDefaultFont()
f, _ := chart.GetDefaultFont()
style := Style{
FontSize: 12,
FontColor: chart.ColorBlack,

View file

@ -27,7 +27,7 @@ import (
"math"
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type pieChart struct {
@ -36,7 +36,6 @@ type pieChart struct {
}
type PieChartOption struct {
// The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@ -52,7 +51,6 @@ 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
@ -63,96 +61,6 @@ 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))
@ -189,105 +97,90 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
seriesNames = seriesList.Names()
}
theme := opt.Theme
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 {
if len(values) == 1 {
seriesPainter.OverrideDrawingStyle(Style{
StrokeWidth: 1,
StrokeColor: s.color,
FillColor: s.color,
StrokeColor: theme.GetSeriesColor(0),
FillColor: theme.GetSeriesColor(0),
})
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
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
}
if s.quadrant == 2 {
if currentQuadrant != 3 {
prevY = s.lineEndY
} else {
prevY = minY
}
// label的角度为饼块中间
angle := start + delta/2
startx := cx + int(radius*math.Cos(angle))
starty := cy + int(radius*math.Sin(angle))
endx := cx + int(labelRadius*math.Cos(angle))
endy := cy + int(labelRadius*math.Sin(angle))
// 计算是否有重叠如果有则调整y坐标位置
if index != 0 &&
math.Abs(float64(endx-prevEndX)) < labelFontSize &&
math.Abs(float64(endy-prevEndY)) < labelFontSize {
endy -= (labelFontSize << 1)
}
if s.quadrant == 3 {
if currentQuadrant != 4 {
prevY = s.lineEndY
} else {
minY = cy * 2
maxY = 0
prevY = 0
}
prevEndX = endx
prevEndY = endy
seriesPainter.MoveTo(startx, starty)
seriesPainter.LineTo(endx, endy)
offset := labelLineWidth
if endx < cx {
offset *= -1
}
if s.quadrant == 4 {
if currentQuadrant != 1 {
prevY = s.lineEndY
} else {
prevY = maxY
}
seriesPainter.MoveTo(endx, endy)
endx += offset
seriesPainter.LineTo(endx, endy)
seriesPainter.Stroke()
textStyle := Style{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
}
currentQuadrant = s.quadrant
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)
}
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

View file

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

View file

@ -29,7 +29,6 @@ import (
const defaultAxisDivideCount = 6
type axisRange struct {
p *Painter
divideCount int
min float64
max float64
@ -38,20 +37,13 @@ type axisRange struct {
}
type AxisRangeOption struct {
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
Min float64
Max float64
Size int
Boundary bool
DivideCount int
}
// NewRange returns a axis range
func NewRange(opt AxisRangeOption) axisRange {
max := opt.Max
min := opt.Min
@ -62,10 +54,7 @@ func NewRange(opt AxisRangeOption) axisRange {
r := math.Abs(max - min)
// 最小单位计算
unit := 1
if r > 5 {
unit = 2
}
unit := 2
if r > 10 {
unit = 4
}
@ -90,12 +79,7 @@ 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,
@ -104,26 +88,18 @@ 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 := formatter(v)
value := commafWithDigits(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))
}
@ -132,13 +108,10 @@ 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)
}

View file

@ -26,7 +26,7 @@ import (
"strings"
"github.com/dustin/go-humanize"
"git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2"
)
type SeriesData struct {
@ -36,7 +36,6 @@ 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 {
@ -45,7 +44,6 @@ 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),
@ -56,7 +54,6 @@ 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 {
@ -79,12 +76,6 @@ 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 (
@ -126,8 +117,6 @@ 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
@ -140,9 +129,6 @@ type Series struct {
type SeriesList []Series
func (sl SeriesList) init() {
if len(sl) == 0 {
return
}
if sl[len(sl)-1].index != 0 {
return
}
@ -173,10 +159,6 @@ 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
}
@ -222,19 +204,13 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
}
type seriesSummary struct {
// 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
MaxIndex int
MaxValue float64
MinIndex int
MinValue float64
AverageValue float64
}
// Summary get summary of series
func (s *Series) Summary() seriesSummary {
minIndex := -1
maxIndex := -1
@ -261,7 +237,6 @@ 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 {
@ -270,10 +245,8 @@ 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}"
@ -281,23 +254,13 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
return NewLabelFormatter(seriesNames, layout)
}
// NewFunnelLabelFormatter returns a funner label formatter
func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter {
if len(layout) == 0 {
layout = "{b}({d})"
}
return NewLabelFormatter(seriesNames, layout)
}
// NewValueLabelFormatter returns a value formatter
func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter {
func NewValueLabelFormater(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

View file

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

View file

@ -1,89 +0,0 @@
// 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))
}

View file

@ -1,254 +0,0 @@
# 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
View file

@ -1,438 +0,0 @@
// 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 = &currentStyle
}
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)
}

View file

@ -1,140 +0,0 @@
// 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))
}
}

View file

@ -24,7 +24,8 @@ package charts
import (
"github.com/golang/freetype/truetype"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
const ThemeDark = "dark"
@ -35,19 +36,12 @@ 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 {
@ -70,25 +64,12 @@ type ThemeOption struct {
SeriesColors []Color
}
var palettes = map[string]*themeColorPalette{}
var palettes = map[string]ColorPalette{}
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"),
@ -239,7 +220,6 @@ func init() {
SetDefaultTheme(ThemeLight)
}
// SetDefaultTheme sets default theme
func SetDefaultTheme(name string) {
defaultTheme = NewTheme(name)
}
@ -260,8 +240,7 @@ func NewTheme(name string) ColorPalette {
if !ok {
p = palettes[ThemeLight]
}
clone := *p
return &clone
return p
}
func (t *themeColorPalette) IsDark() bool {
@ -272,42 +251,23 @@ 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
@ -315,18 +275,10 @@ 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, _ := GetDefaultFont()
f, _ := chart.GetDefaultFont()
return f
}
func (t *themeColorPalette) SetFont(f *truetype.Font) {
t.font = f
}

View file

@ -36,6 +36,10 @@ 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.
@ -80,7 +84,6 @@ type titlePainter struct {
opt *TitleOption
}
// NewTitlePainter returns a title renderer
func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter {
return &titlePainter{
p: p,
@ -93,9 +96,6 @@ 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
}

View file

@ -1,93 +0,0 @@
// 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
View file

@ -29,8 +29,8 @@ import (
"strings"
"github.com/dustin/go-humanize"
"git.smarteching.com/zeni/go-chart/v2"
"git.smarteching.com/zeni/go-chart/v2/drawing"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
func TrueFlag() *bool {
@ -90,30 +90,6 @@ 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
@ -160,25 +136,15 @@ 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
if value >= T_VALUE {
return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T"
m := float64(1000 * 1000)
if value >= m {
return humanize.CommafWithDigits(value/m, decimals) + "M"
}
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"
k := float64(1000)
if value >= k {
return humanize.CommafWithDigits(value/k, decimals) + "k"
}
return humanize.CommafWithDigits(value, decimals)
}
@ -262,10 +228,3 @@ func getPolygonPoints(center Point, radius float64, sides int) []Point {
}
return points
}
func isLightColor(c Color) bool {
r := float64(c.R) * float64(c.R) * 0.299
g := float64(c.G) * float64(c.G) * 0.587
b := float64(c.B) * float64(c.B) * 0.114
return math.Sqrt(r+g+b) > 127.5
}

View file

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

View file

@ -47,19 +47,12 @@ type XAxisOption struct {
// The line color of axis
StrokeColor Color
// The color of label
FontColor Color
// The text rotation of label
TextRotation float64
// The first axis
FirstAxis int
// The offset of label
LabelOffset Box
FontColor Color
isValueAxis bool
}
const defaultXAxisHeight = 30
// NewXAxisOption returns a x axis option
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
opt := XAxisOption{
Data: data,
@ -87,9 +80,6 @@ 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
@ -99,7 +89,6 @@ 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())
}

View file

@ -47,14 +47,9 @@ 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,
@ -69,18 +64,14 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption {
return opts
}
func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
func (opt *YAxisOption) ToAxisOption() AxisOption {
position := PositionLeft
if opt.Position == PositionRight {
position = PositionRight
}
theme := opt.Theme
if theme == nil {
theme = p.theme
}
axisOpt := AxisOption{
Formatter: opt.Formatter,
Theme: theme,
Theme: opt.Theme,
Data: opt.Data,
Position: position,
FontSize: opt.FontSize,
@ -89,9 +80,8 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
FontColor: opt.FontColor,
BoundaryGap: FalseFlag(),
SplitLineShow: true,
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
Show: opt.Show,
Unit: opt.Unit,
}
if !opt.Color.IsZero() {
axisOpt.FontColor = opt.Color
@ -102,26 +92,21 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) 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(p))
return NewAxisPainter(p, opt.ToAxisOption())
}
// NewRightYAxis returns a right y axis renderer
func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
p = p.Child(PainterPaddingOption(Box{
Bottom: defaultXAxisHeight,
}))
axisOpt := opt.ToAxisOption(p)
axisOpt := opt.ToAxisOption()
axisOpt.Position = PositionRight
axisOpt.SplitLineShow = false
return NewAxisPainter(p, axisOpt)

View file

@ -1,70 +0,0 @@
// 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))
}
}