diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ebc9a02..ce56fe7 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,11 +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
diff --git a/.gitignore b/.gitignore
index 2ac8a25..57206ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
*.png
-*.svg
\ No newline at end of file
+*.svg
+tmp
+NotoSansSC.ttf
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index ff4b974..0650395 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,457 @@
-# go-echarts
+# go-charts
-[](https://github.com/vicanso/go-echarts/blob/master/LICENSE)
-[](https://github.com/vicanso/go-echarts/actions)
+Clone from https://github.com/vicanso/go-charts
-`go-echarts`基于[go-chart](https://github.com/wcharczuk/go-chart)生成数据图表,支持`svg`与`png`的输出,`Apache ECharts`在前端开发中得到众多开发者的认可,`go-echarts`兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的几种图表截图(黑夜模式):
+[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
+[](https://github.com/vicanso/go-charts/actions)
-
+[中文](./README_zh.md)
-## 支持图表类型
+`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`.
-暂仅支持三种的图表类型:`line`, `bar` 以及 `pie`
+`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`.
+
+Screenshot of common charts, the left part is light theme, the right part is grafana theme.
+
+
+
+
+
+
+
+
+
+## Chart Type
+
+These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`.
+
+## Example
+
+More examples can be found in the [./examples/](./examples/) directory.
-## 示例
+### Line Chart
+```go
+package main
-`go-echarts`兼容了`echarts`的参数配置,可简单的使用json形式的配置字符串则可快速生成图表。
+import (
+ charts "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func main() {
+ values := [][]float64{
+ {
+ 120,
+ 132,
+ 101,
+ 134,
+ 90,
+ 230,
+ 210,
+ },
+ {
+ // snip...
+ },
+ {
+ // snip...
+ },
+ {
+ // snip...
+ },
+ {
+ // snip...
+ },
+ }
+ p, err := charts.LineRender(
+ values,
+ charts.TitleTextOptionFunc("Line"),
+ charts.XAxisDataOptionFunc([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }),
+ charts.LegendLabelsOptionFunc([]string{
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ "Direct",
+ "Search Engine",
+ }, charts.PositionCenter),
+ )
+
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### Bar Chart
```go
package main
import (
- "os"
+ "git.smarteching.com/zeni/go-charts/v2"
+)
- charts "github.com/vicanso/go-echarts"
+func main() {
+ values := [][]float64{
+ {
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ },
+ {
+ // snip...
+ },
+ }
+ p, err := charts.BarRender(
+ values,
+ charts.XAxisDataOptionFunc([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ charts.LegendLabelsOptionFunc([]string{
+ "Rainfall",
+ "Evaporation",
+ }, charts.PositionRight),
+ charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage),
+ charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin),
+ // custom option func
+ func(opt *charts.ChartOption) {
+ opt.SeriesList[1].MarkPoint = charts.NewMarkPoint(
+ charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin,
+ )
+ opt.SeriesList[1].MarkLine = charts.NewMarkLine(
+ charts.SeriesMarkDataTypeAverage,
+ )
+ },
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### Horizontal Bar Chart
+
+```go
+package main
+
+import (
+ "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func main() {
+ values := [][]float64{
+ {
+ 18203,
+ 23489,
+ 29034,
+ 104970,
+ 131744,
+ 630230,
+ },
+ {
+ // snip...
+ },
+ }
+ p, err := charts.HorizontalBarRender(
+ values,
+ charts.TitleTextOptionFunc("World Population"),
+ charts.PaddingOptionFunc(charts.Box{
+ Top: 20,
+ Right: 40,
+ Bottom: 20,
+ Left: 20,
+ }),
+ charts.LegendLabelsOptionFunc([]string{
+ "2011",
+ "2012",
+ }),
+ charts.YAxisDataOptionFunc([]string{
+ "Brazil",
+ "Indonesia",
+ "USA",
+ "India",
+ "China",
+ "World",
+ }),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### Pie Chart
+
+```go
+package main
+
+import (
+ "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func main() {
+ values := []float64{
+ 1048,
+ 735,
+ 580,
+ 484,
+ 300,
+ }
+ p, err := charts.PieRender(
+ values,
+ charts.TitleOptionFunc(charts.TitleOption{
+ Text: "Rainfall vs Evaporation",
+ Subtext: "Fake Data",
+ Left: charts.PositionCenter,
+ }),
+ charts.PaddingOptionFunc(charts.Box{
+ Top: 20,
+ Right: 20,
+ Bottom: 20,
+ Left: 20,
+ }),
+ charts.LegendOptionFunc(charts.LegendOption{
+ Orient: charts.OrientVertical,
+ Data: []string{
+ "Search Engine",
+ "Direct",
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ },
+ Left: charts.PositionLeft,
+ }),
+ charts.PieSeriesShowLabel(),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### Radar Chart
+
+```go
+package main
+
+import (
+ "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func main() {
+ values := [][]float64{
+ {
+ 4200,
+ 3000,
+ 20000,
+ 35000,
+ 50000,
+ 18000,
+ },
+ {
+ // snip...
+ },
+ }
+ p, err := charts.RadarRender(
+ values,
+ charts.TitleTextOptionFunc("Basic Radar Chart"),
+ charts.LegendLabelsOptionFunc([]string{
+ "Allocated Budget",
+ "Actual Spending",
+ }),
+ charts.RadarIndicatorOptionFunc([]string{
+ "Sales",
+ "Administration",
+ "Information Technology",
+ "Customer Support",
+ "Development",
+ "Marketing",
+ }, []float64{
+ 6500,
+ 16000,
+ 30000,
+ 38000,
+ 52000,
+ 25000,
+ }),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### Funnel Chart
+
+```go
+package main
+
+import (
+ "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func main() {
+ values := []float64{
+ 100,
+ 80,
+ 60,
+ 40,
+ 20,
+ }
+ p, err := charts.FunnelRender(
+ values,
+ charts.TitleTextOptionFunc("Funnel"),
+ charts.LegendLabelsOptionFunc([]string{
+ "Show",
+ "Click",
+ "Visit",
+ "Inquiry",
+ "Order",
+ }),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ // snip...
+}
+```
+
+### 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"
)
func main() {
@@ -31,7 +460,6 @@ func main() {
"text": "Line"
},
"xAxis": {
- "type": "category",
"data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
"series": [
@@ -40,43 +468,75 @@ func main() {
}
]
}`)
- if err != nil {
- panic(err)
- }
- os.WriteFile("output.png", buf, 0600)
+ // snip...
}
```
-## 参数说明
+## ECharts Option
-- `theme` 颜色主题,支持`dark`与`light`模式,默认为`light`
-- `padding` 图表的内边距,单位px。支持以下几种模式的设置
- - `padding: 5` 设置内边距为5
- - `padding: [5, 10]` 设置上下的内边距为 5,左右的内边距为 10
- - `padding:[5, 10, 5, 10]` 分别设置`上右下左`边距
-- `title` 图表标题,包括标题内容、高度、颜色等
- - `title.text` 标题内容
- - `title.textStyle.color` 标题文字颜色
- - `title.textStyle.fontSize` 标题文字字体大小
- - `title.textStyle.height` 标题高度
-- `xAxis` 直角坐标系grid中的x轴,由于go-echarts仅支持单一个x轴,因此若参数为数组多个x轴,只使用第一个配置
- - `xAxis.boundaryGap` 坐标轴两边留白策略,仅支持三种设置方式`null`, `true`或者`false`。`null`或`true`时则数据点展示在两个刻度中间
- - `xAxis.splitNumber` 坐标轴的分割段数,需要注意的是这个分割段数只是个预估值,最后实际显示的段数会在这个基础上根据分割后坐标轴刻度显示的易读程度作调整
- - `xAxis.data` x轴的展示文案,暂只支持字符串数组,如["Mon", "Tue"],其数量需要与展示点一致
-- `yAxis` 直角坐标系grid中的y轴,最多支持两个y轴
- - `yAxis.min` 坐标轴刻度最小值,若不设置则自动计算
- - `yAxis.max` 坐标轴刻度最大值,若不设置则自动计算
- - `yAxis.axisLabel.formatter` 刻度标签的内容格式器,如`"formatter": "{value} kg"`
-- `legend` 图表中不同系列的标记
- - `legend.data` 图例的数据数组,为字符串数组,如["Email", "Video Ads"]
- - `legend.align` 图例标记和文本的对齐,默认为标记靠左`left`
- - `legend.padding` legend的padding,配置方式与图表的`padding`一致
- - `legend.left` legend离容器左侧的距离,其值可以为具体的像素值(20)或百分比(20%)
- - `legend.right` legend离容器右侧的距离,其值可以为具体的像素值(20)或百分比(20%)
-- `series` 图表的数据项列表
- - `series.type` 图表的展示类型,暂支持`line`, `bar`以及`pie`,需要注意`pie`只能单独使用
- - `series.yAxisIndex` 该数据项使用的y轴,默认为0,对yAxis的配置对应
- - `series.itemStyle.color` 该数据项展示时使用的颜色
- - `series.data` 数据项对应的数据数组,支持以下形式的数据:
- - `数值` 常用形式,数组数据为浮点数组,如[1.1, 2,3, 5.2]
- - `结构体` pie图表或bar图表中指定样式使用,如[{"value": 1048, "name": "Search Engine"},{"value": 735,"name": "Direct"}]
\ No newline at end of file
+The name with `[]` is new parameter, others are the same as `echarts`.
+
+- `[type]` The canvas type, support `svg` and `png`, default is `svg`
+- `[theme]` The theme, support `dark`, `light` and `grafana`, default is `light`
+- `[fontFamily]` The font family for chart
+- `[padding]` The padding of chart
+- `[box]` The canvas box of chart
+- `[width]` The width of chart
+- `[height]` The height of chart
+- `title` Title component, including main title and subtitle
+ - `title.text` The main title text, supporting for \n for newlines
+ - `title.subtext`Subtitle text, supporting for \n for newlines
+ - `title.left` Distance between title component and the left side of the container. Left value can be instant pixel value like 20; it can also be a percentage value relative to container width like '20%'; and it can also be 'left', 'center', or 'right'.
+ - `title.top` Distance between title component and the top side of the container. Top value can be instant pixel value like 20
+ - `title.textStyle.color` Text color for title
+ - `title.textStyle.fontSize` Text font size for title
+ - `title.textStyle.fontFamily` Text font family for title, it will change the font family for chart
+- `xAxis` The x axis in cartesian(rectangular) coordinate. `go-charts` only support one x axis.
+ - `xAxis.boundaryGap` The boundary gap on both sides of a coordinate axis. The setting and behavior of category axes and non-category axes are different. If set `null` or `true`, the label appear in the center part of two axis ticks.
+ - `xAxis.splitNumber` Number of segments that the axis is split into. Note that this number serves only as a recommendation, and the true segments may be adjusted based on readability
+ - `xAxis.data` Category data, only support string array.
+- `yAxis` The y axis in cartesian(rectangular) coordinate, it support 2 y axis
+ - `yAxis.min` The minimum value of axis. It will be automatically computed to make sure axis tick is equally distributed when not set
+ - `yAxis.max` The maximum value of axis. It will be automatically computed to make sure axis tick is equally distributed when not se.
+ - `yAxis.axisLabel.formatter` Formatter of axis label, which supports string template: `"formatter": "{value} kg"`
+ - `yAxis.axisLine.lineStyle.color` The color for line
+- `legend` Legend component
+ - `legend.show` Whether to show legend
+ - `legend.data` Data array of legend, only support string array: ["Email", "Video Ads"]
+ - `legend.align` Legend marker and text aligning. Support `left` and `right`, default is `left`
+ - `legend.padding` legend space around content
+ - `legend.left` Distance between legend component and the left side of the container. Left value can be instant pixel value like 20; it can also be a percentage value relative to container width like '20%'; and it can also be 'left', 'center', or 'right'.
+ - `legend.top` Distance between legend component and the top side of the container. Top value can be instant pixel value like 20
+- `radar` Coordinate for radar charts
+ - `radar.indicator` Indicator of radar chart, which is used to assign multiple variables(dimensions) in radar chart
+ - `radar.indicator.name` Indicator's name
+ - `radar.indicator.max` The maximum value of indicator
+ - `radar.indicator.min` The minimum value of indicator, default value is 0.
+- `series` The series for chart
+ - `series.name` Series name used for displaying in legend.
+ - `series.type` Series type: `line`, `bar`, `pie`, `radar` or `funnel`
+ - `series.radius` Radius of Pie chart:`50%`, default is `40%`
+ - `series.yAxisIndex` Index of y axis to combine with, which is useful for multiple y axes in one chart
+ - `series.label.show` Whether to show label
+ - `series.label.distance` Distance to the host graphic element
+ - `series.label.color` Label color
+ - `series.itemStyle.color` Color for the series's item
+ - `series.markPoint` Mark point in a chart.
+ - `series.markPoint.symbolSize` Symbol size, default is `30`
+ - `series.markPoint.data` Data array for mark points, each of which is an object and the type only support `max` and `min`: `[{"type": "max"}, {"type": "min"}]`
+ - `series.markLine` Mark line in a chart
+ - `series.markPoint.data` Data array for mark points, each of which is an object and the type only support `max`, `min` and `average`: `[{"type": "max"}, {"type": "min"}, {"type": "average"}]``
+ - `series.data` Data array of series, which can be in the following forms:
+ - `value` It's a float array: [1.1, 2,3, 5.2]
+ - `object` It's a object value array: [{"value": 1048, "name": "Search Engine"},{"value": 735,"name": "Direct"}]
+- `[children]` The options of children chart
+
+
+## Performance
+
+Generate a png chart will be less than 20ms. It's better than using `chrome headless` with `echarts`.
+
+```bash
+BenchmarkMultiChartPNGRender-8 78 15216336 ns/op 2298308 B/op 1148 allocs/op
+BenchmarkMultiChartSVGRender-8 367 3356325 ns/op 20597282 B/op 3088 allocs/op
+```
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..3f35b97
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,576 @@
+# go-charts
+
+[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
+[](https://github.com/vicanso/go-charts/actions)
+
+`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。
+
+`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana):
+
+
+
+
+
+
+
+
+
result {
- result = v
+type AxisOption struct {
+ // The theme of chart
+ Theme ColorPalette
+ // Formatter for y axis text value
+ Formatter string
+ // The label of axis
+ Data []string
+ // The boundary gap on both sides of a coordinate axis.
+ // Nil or *true means the center part of two axis ticks
+ BoundaryGap *bool
+ // The flag for show axis, set this to *false will hide axis
+ Show *bool
+ // The position of axis, it can be 'left', 'top', 'right' or 'bottom'
+ Position string
+ // Number of segments that the axis is split into. Note that this number serves only as a recommendation.
+ SplitNumber int
+ // The line color of axis
+ StrokeColor Color
+ // The line width
+ 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
+ FontSize float64
+ // The font of label
+ Font *truetype.Font
+ // The color of label
+ FontColor Color
+ // The flag for show axis split line, set this to true will show axis split line
+ 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) {
+ opt := a.opt
+ top := a.p
+ theme := opt.Theme
+ if theme == nil {
+ theme = top.theme
+ }
+ if isFalse(opt.Show) {
+ return BoxZero, nil
+ }
+
+ strokeWidth := opt.StrokeWidth
+ if strokeWidth == 0 {
+ strokeWidth = 1
+ }
+
+ font := opt.Font
+ if font == nil {
+ font = a.p.font
+ }
+ if font == nil {
+ font = theme.GetFont()
+ }
+ fontColor := opt.FontColor
+ if fontColor.IsZero() {
+ fontColor = theme.GetTextColor()
+ }
+ fontSize := opt.FontSize
+ if fontSize == 0 {
+ fontSize = theme.GetFontSize()
+ }
+ strokeColor := opt.StrokeColor
+ if strokeColor.IsZero() {
+ strokeColor = theme.GetAxisStrokeColor()
+ }
+
+ data := opt.Data
+ formatter := opt.Formatter
+ if len(formatter) != 0 {
+ for index, text := range data {
+ data[index] = strings.ReplaceAll(formatter, "{value}", text)
}
}
- return result
-}
+ dataCount := len(data)
+ tickCount := dataCount
-// GetXAxisAndValues returns x axis by theme, and the values of axis.
-func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) {
- data := xAxis.Data
- originalSize := len(data)
- // 如果居中,则需要多添加一个值
- if tickPosition == chart.TickPositionBetweenTicks {
- data = append([]string{
- "",
- }, data...)
+ boundaryGap := true
+ if isFalse(opt.BoundaryGap) {
+ boundaryGap = false
+ }
+ isVertical := opt.Position == PositionLeft ||
+ opt.Position == PositionRight
+
+ labelPosition := ""
+ if !boundaryGap {
+ tickCount--
+ labelPosition = PositionLeft
+ }
+ if isVertical && boundaryGap {
+ labelPosition = PositionCenter
}
- size := len(data)
+ // 如果小于0,则表示不处理
+ tickLength := getDefaultInt(opt.TickLength, 5)
+ labelMargin := getDefaultInt(opt.LabelMargin, 5)
- xValues := make([]float64, size)
- ticks := make([]chart.Tick, 0)
-
- // tick width
- maxTicks := maxInt(xAxis.SplitNumber, 10)
-
- // 计息最多每个unit至少放多个
- minUnitSize := originalSize / maxTicks
- if originalSize%maxTicks != 0 {
- minUnitSize++
+ style := Style{
+ StrokeColor: strokeColor,
+ StrokeWidth: strokeWidth,
+ Font: font,
+ FontColor: fontColor,
+ FontSize: fontSize,
}
- unitSize := minUnitSize
- // 尽可能选择一格展示更多的块
- for i := minUnitSize; i < 2*minUnitSize; i++ {
- if originalSize%i == 0 {
- unitSize = i
+ 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++
}
}
- for index, key := range data {
- f := float64(index)
- xValues[index] = f
- if index%unitSize == 0 || index == size-1 {
- ticks = append(ticks, chart.Tick{
- Value: f,
- Label: key,
- })
+ width := 0
+ height := 0
+ // 垂直
+ if isVertical {
+ width = textMaxWidth + tickLength<<1
+ height = top.Height()
+ } else {
+ width = top.Width()
+ height = tickLength<<1 + textMaxHeight
+ }
+ padding := Box{}
+ switch opt.Position {
+ case PositionTop:
+ padding.Top = top.Height() - height
+ case PositionLeft:
+ padding.Right = top.Width() - width
+ case PositionRight:
+ padding.Left = top.Width() - width
+ default:
+ padding.Top = top.Height() - defaultXAxisHeight
+ }
+
+ p := top.Child(PainterPaddingOption(padding))
+
+ x0 := 0
+ y0 := 0
+ x1 := 0
+ y1 := 0
+ ticksPaddingTop := 0
+ ticksPaddingLeft := 0
+ labelPaddingTop := 0
+ labelPaddingLeft := 0
+ labelPaddingRight := 0
+ orient := ""
+ textAlign := ""
+
+ switch opt.Position {
+ case PositionTop:
+ labelPaddingTop = 0
+ x1 = p.Width()
+ y0 = labelMargin + int(opt.FontSize)
+ ticksPaddingTop = int(opt.FontSize)
+ y1 = y0
+ orient = OrientHorizontal
+ case PositionLeft:
+ x0 = p.Width()
+ y0 = 0
+ x1 = p.Width()
+ y1 = p.Height()
+ orient = OrientVertical
+ textAlign = AlignRight
+ ticksPaddingLeft = textMaxWidth + tickLength
+ labelPaddingRight = width - textMaxWidth
+ case PositionRight:
+ orient = OrientVertical
+ y1 = p.Height()
+ labelPaddingLeft = width - textMaxWidth
+ default:
+ labelPaddingTop = height
+ x1 = p.Width()
+ orient = OrientHorizontal
+ }
+
+ if strokeWidth > 0 {
+ p.Child(PainterPaddingOption(Box{
+ Top: ticksPaddingTop,
+ Left: ticksPaddingLeft,
+ })).Ticks(TicksOption{
+ Count: tickCount,
+ Length: tickLength,
+ Unit: unit,
+ Orient: orient,
+ First: opt.FirstAxis,
+ })
+ p.LineStroke([]Point{
+ {
+ X: x0,
+ Y: y0,
+ },
+ {
+ X: x1,
+ Y: y1,
+ },
+ })
+ }
+
+ p.Child(PainterPaddingOption(Box{
+ Left: labelPaddingLeft,
+ 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,
+ })
+ // 显示辅助线
+ if opt.SplitLineShow {
+ style.StrokeColor = opt.SplitLineColor
+ style.StrokeWidth = 1
+ top.OverrideDrawingStyle(style)
+ if isVertical {
+ x0 := p.Width()
+ x1 := top.Width()
+ if opt.Position == PositionRight {
+ x0 = 0
+ x1 = top.Width() - p.Width()
+ }
+ yValues := autoDivide(height, tickCount)
+ yValues = yValues[0 : len(yValues)-1]
+ for _, y := range yValues {
+ top.LineStroke([]Point{
+ {
+ X: x0,
+ Y: y,
+ },
+ {
+ X: x1,
+ Y: y,
+ },
+ })
+ }
+ } else {
+ y0 := p.Height() - defaultXAxisHeight
+ y1 := top.Height() - defaultXAxisHeight
+ for index, x := range autoDivide(width, tickCount) {
+ if index == 0 {
+ continue
+ }
+ top.LineStroke([]Point{
+ {
+ X: x,
+ Y: y0,
+ },
+ {
+ X: x,
+ Y: y1,
+ },
+ })
+ }
}
}
- return chart.XAxis{
- Ticks: ticks,
- TickPosition: tickPosition,
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- StrokeColor: getAxisColor(theme),
- StrokeWidth: axisStrokeWidth,
- },
- }, xValues
-}
-func defaultFloatFormater(v interface{}) string {
- value, ok := v.(float64)
- if !ok {
- return ""
- }
- // 大于10的则直接取整展示
- if value >= 10 {
- return humanize.CommafWithDigits(value, 0)
- }
- return humanize.CommafWithDigits(value, 2)
-}
-
-// GetSecondaryYAxis returns the secondary y axis by theme
-func GetSecondaryYAxis(theme string, option *YAxisOption) chart.YAxis {
- strokeColor := getGridColor(theme)
- yAxis := chart.YAxis{
- Range: &chart.ContinuousRange{},
- ValueFormatter: defaultFloatFormater,
- AxisType: chart.YAxisSecondary,
- GridMajorStyle: chart.Style{
- StrokeColor: strokeColor,
- StrokeWidth: axisStrokeWidth,
- },
- GridMinorStyle: chart.Style{
- StrokeColor: strokeColor,
- StrokeWidth: axisStrokeWidth,
- },
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- // alpha 0,隐藏
- StrokeColor: hiddenColor,
- StrokeWidth: axisStrokeWidth,
- },
- }
- setYAxisOption(&yAxis, option)
- return yAxis
-}
-
-func setYAxisOption(yAxis *chart.YAxis, option *YAxisOption) {
- if option == nil {
- return
- }
- if option.Formater != nil {
- yAxis.ValueFormatter = option.Formater
- }
- if option.Max != nil {
- yAxis.Range.SetMax(*option.Max)
- }
- if option.Min != nil {
- yAxis.Range.SetMin(*option.Min)
- }
-}
-
-// GetYAxis returns the primary y axis by theme
-func GetYAxis(theme string, option *YAxisOption) chart.YAxis {
- disabled := false
- if option != nil {
- disabled = option.Disabled
- }
- hidden := chart.Hidden()
-
- yAxis := chart.YAxis{
- Range: &chart.ContinuousRange{},
- ValueFormatter: defaultFloatFormater,
- AxisType: chart.YAxisPrimary,
- GridMajorStyle: hidden,
- GridMinorStyle: hidden,
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- // alpha 0,隐藏
- StrokeColor: hiddenColor,
- StrokeWidth: axisStrokeWidth,
- },
- }
- // 如果禁用,则默认为隐藏,并设置range
- if disabled {
- yAxis.Range = &HiddenRange{}
- yAxis.Style.Hidden = true
- }
- setYAxisOption(&yAxis, option)
- return yAxis
+ return Box{
+ Bottom: height,
+ Right: width,
+ }, nil
}
diff --git a/axis_test.go b/axis_test.go
index 8396e8a..85e18ca 100644
--- a/axis_test.go
+++ b/axis_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,150 +23,151 @@
package charts
import (
- "strconv"
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
+ "git.smarteching.com/zeni/go-chart/v2/drawing"
)
-func TestGetXAxisAndValues(t *testing.T) {
+func TestAxis(t *testing.T) {
assert := assert.New(t)
-
- genLabels := func(count int) []string {
- arr := make([]string, count)
- for i := 0; i < count; i++ {
- arr[i] = strconv.Itoa(i)
- }
- return arr
- }
- genValues := func(count int, betweenTicks bool) []float64 {
- if betweenTicks {
- count++
- }
- arr := make([]float64, count)
- for i := 0; i < count; i++ {
- arr[i] = float64(i)
- }
- return arr
- }
- genTicks := func(count int, betweenTicks bool) []chart.Tick {
- arr := make([]chart.Tick, 0)
- offset := 0
- if betweenTicks {
- offset = 1
- arr = append(arr, chart.Tick{})
- }
- for i := 0; i < count; i++ {
- arr = append(arr, chart.Tick{
- Value: float64(i + offset),
- Label: strconv.Itoa(i),
- })
- }
- return arr
- }
-
tests := []struct {
- xAxis XAxis
- tickPosition chart.TickPosition
- theme string
- result chart.XAxis
- values []float64
+ render func(*Painter) ([]byte, error)
+ result string
}{
+ // 底部x轴
{
- xAxis: XAxis{
- Data: genLabels(5),
- },
- values: genValues(5, false),
- result: chart.XAxis{
- Ticks: genTicks(5, false),
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ SplitLineShow: true,
+ SplitLineColor: drawing.ColorBlack,
+ }).Render()
+ return p.Bytes()
},
+ result: "",
},
- // 居中
+ // 底部x轴文本居左
{
- xAxis: XAxis{
- Data: genLabels(5),
- },
- tickPosition: chart.TickPositionBetweenTicks,
- // 居中因此value多一个
- values: genValues(5, true),
- result: chart.XAxis{
- Ticks: genTicks(5, true),
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ BoundaryGap: FalseFlag(),
+ }).Render()
+ return p.Bytes()
},
+ result: "",
},
+ // 左侧y轴
{
- xAxis: XAxis{
- Data: genLabels(20),
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ Position: PositionLeft,
+ }).Render()
+ return p.Bytes()
},
- // 居中因此value多一个
- values: genValues(20, false),
- result: chart.XAxis{
- Ticks: []chart.Tick{
- {Value: 0, Label: "0"}, {Value: 2, Label: "2"}, {Value: 4, Label: "4"}, {Value: 6, Label: "6"}, {Value: 8, Label: "8"}, {Value: 10, Label: "10"}, {Value: 12, Label: "12"}, {Value: 14, Label: "14"}, {Value: 16, Label: "16"}, {Value: 18, Label: "18"}, {Value: 19, Label: "19"}},
+ result: "",
+ },
+ // 左侧y轴居中
+ {
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ Position: PositionLeft,
+ BoundaryGap: FalseFlag(),
+ SplitLineShow: true,
+ SplitLineColor: drawing.ColorBlack,
+ }).Render()
+ return p.Bytes()
},
+ result: "",
+ },
+ // 右侧
+ {
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ Position: PositionRight,
+ BoundaryGap: FalseFlag(),
+ SplitLineShow: true,
+ SplitLineColor: drawing.ColorBlack,
+ }).Render()
+ return p.Bytes()
+ },
+ result: "",
+ },
+ // 顶部
+ {
+ render: func(p *Painter) ([]byte, error) {
+ _, _ = NewAxisPainter(p, AxisOption{
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ Formatter: "{value} --",
+ Position: PositionTop,
+ }).Render()
+ return p.Bytes()
+ },
+ result: "",
},
}
for _, tt := range tests {
- xAxis, values := GetXAxisAndValues(tt.xAxis, tt.tickPosition, tt.theme)
-
- assert.Equal(tt.result.Ticks, xAxis.Ticks)
- assert.Equal(tt.tickPosition, xAxis.TickPosition)
- assert.Equal(tt.values, values)
+ 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))
}
}
-
-func TestDefaultFloatFormater(t *testing.T) {
- assert := assert.New(t)
-
- assert.Equal("", defaultFloatFormater(1))
-
- assert.Equal("0.1", defaultFloatFormater(0.1))
- assert.Equal("0.12", defaultFloatFormater(0.123))
- assert.Equal("10", defaultFloatFormater(10.1))
-}
-
-func TestSetYAxisOption(t *testing.T) {
- assert := assert.New(t)
- min := 10.0
- max := 20.0
- opt := &YAxisOption{
- Formater: func(v interface{}) string {
- return ""
- },
- Min: &min,
- Max: &max,
- }
- yAxis := &chart.YAxis{
- Range: &chart.ContinuousRange{},
- }
- setYAxisOption(yAxis, opt)
-
- assert.NotEmpty(yAxis.ValueFormatter)
- assert.Equal(max, yAxis.Range.GetMax())
- assert.Equal(min, yAxis.Range.GetMin())
-}
-
-func TestGetYAxis(t *testing.T) {
- assert := assert.New(t)
-
- yAxis := GetYAxis(ThemeDark, nil)
-
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.False(yAxis.Style.Hidden)
-
- yAxis = GetYAxis(ThemeDark, &YAxisOption{
- Disabled: true,
- })
-
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.Style.Hidden)
-
- // secondary yAxis
- yAxis = GetSecondaryYAxis(ThemeDark, nil)
- assert.False(yAxis.GridMajorStyle.Hidden)
- assert.False(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.Style.StrokeColor.IsZero())
-}
diff --git a/bar_chart.go b/bar_chart.go
new file mode 100644
index 0000000..043e044
--- /dev/null
+++ b/bar_chart.go
@@ -0,0 +1,253 @@
+// 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 (
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ "git.smarteching.com/zeni/go-chart/v2"
+)
+
+type barChart struct {
+ p *Painter
+ opt *BarChartOption
+}
+
+// NewBarChart returns a bar chart renderer
+func NewBarChart(p *Painter, opt BarChartOption) *barChart {
+ if opt.Theme == nil {
+ opt.Theme = defaultTheme
+ }
+ return &barChart{
+ p: p,
+ opt: &opt,
+ }
+}
+
+type BarChartOption struct {
+ // The theme
+ Theme ColorPalette
+ // The font size
+ Font *truetype.Font
+ // The data series list
+ SeriesList SeriesList
+ // The x axis option
+ XAxis XAxisOption
+ // The padding of line chart
+ Padding Box
+ // The y axis option
+ YAxisOptions []YAxisOption
+ // The option of title
+ Title TitleOption
+ // The legend option
+ Legend LegendOption
+ BarWidth int
+ // Margin of bar
+ BarMargin int
+}
+
+func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
+ p := b.p
+ opt := b.opt
+ seriesPainter := result.seriesPainter
+
+ xRange := NewRange(AxisRangeOption{
+ Painter: b.p,
+ DivideCount: len(opt.XAxis.Data),
+ Size: seriesPainter.Width(),
+ })
+ x0, x1 := xRange.GetRange(0)
+ width := int(x1 - x0)
+ // 每一块之间的margin
+ margin := 10
+ // 每一个bar之间的margin
+ barMargin := 5
+ if width < 20 {
+ margin = 2
+ barMargin = 2
+ } else if width < 50 {
+ 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
+ }
+ barMaxHeight := seriesPainter.Height()
+ theme := opt.Theme
+ seriesNames := seriesList.Names()
+
+ markPointPainter := NewMarkPointPainter(seriesPainter)
+ markLinePainter := NewMarkLinePainter(seriesPainter)
+ rendererList := []Renderer{
+ markPointPainter,
+ markLinePainter,
+ }
+ for index := range seriesList {
+ series := seriesList[index]
+ yRange := result.axisRanges[series.AxisIndex]
+ seriesColor := theme.GetSeriesColor(series.index)
+
+ 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
+ }
+ x := divideValues[j]
+ x += margin
+ if index != 0 {
+ x += index * (barWidth + barMargin)
+ }
+
+ h := int(yRange.getHeight(item.Value))
+ fillColor := seriesColor
+ if !item.Style.FillColor.IsZero() {
+ fillColor = item.Style.FillColor
+ }
+ 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)
+ }
+ // 用于生成marker point
+ points[j] = Point{
+ // 居中的位置
+ X: x + barWidth>>1,
+ Y: top,
+ }
+ // 用于生成marker point
+ points[j] = Point{
+ // 居中的位置
+ X: x + barWidth>>1,
+ Y: top,
+ }
+ // 如果label不需要展示,则返回
+ if labelPainter == nil {
+ 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
+ }
+ }
+ }
+ 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,
+ })
+ }
+
+ markPointPainter.Add(markPointRenderOption{
+ FillColor: seriesColor,
+ Font: opt.Font,
+ Series: series,
+ Points: points,
+ })
+ markLinePainter.Add(markLineRenderOption{
+ FillColor: seriesColor,
+ FontColor: opt.Theme.GetTextColor(),
+ StrokeColor: seriesColor,
+ Font: opt.Font,
+ Series: series,
+ Range: yRange,
+ })
+ }
+ // 最大、最小的mark point
+ err := doRender(rendererList...)
+ if err != nil {
+ return BoxZero, err
+ }
+
+ return p.box, nil
+}
+
+func (b *barChart) Render() (Box, error) {
+ p := b.p
+ opt := b.opt
+ renderResult, err := defaultRender(p, defaultRenderOption{
+ Theme: opt.Theme,
+ Padding: opt.Padding,
+ SeriesList: opt.SeriesList,
+ XAxis: opt.XAxis,
+ YAxisOptions: opt.YAxisOptions,
+ TitleOption: opt.Title,
+ LegendOption: opt.Legend,
+ })
+ if err != nil {
+ return BoxZero, err
+ }
+ seriesList := opt.SeriesList.Filter(ChartTypeLine)
+ return b.render(renderResult, seriesList)
+}
diff --git a/bar_chart_test.go b/bar_chart_test.go
new file mode 100644
index 0000000..654c320
--- /dev/null
+++ b/bar_chart_test.go
@@ -0,0 +1,190 @@
+// 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 TestBarChart(t *testing.T) {
+ assert := assert.New(t)
+
+ tests := []struct {
+ render func(*Painter) ([]byte, error)
+ result string
+ }{
+ {
+ render: func(p *Painter) ([]byte, error) {
+ seriesList := NewSeriesListDataFromValues([][]float64{
+ {
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ },
+ {
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ },
+ })
+ for index := range seriesList {
+ seriesList[index].Label.Show = true
+ }
+ _, err := NewBarChart(p, BarChartOption{
+ Padding: Box{
+ Left: 10,
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ },
+ SeriesList: seriesList,
+ XAxis: NewXAxisOption([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ YAxisOptions: NewYAxisOptions([]string{
+ "Rainfall",
+ "Evaporation",
+ }),
+ }).Render()
+ if err != nil {
+ return nil, err
+ }
+ return p.Bytes()
+ },
+ result: "",
+ },
+ {
+ render: func(p *Painter) ([]byte, error) {
+ seriesList := NewSeriesListDataFromValues([][]float64{
+ {
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ },
+ {
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ },
+ })
+ for index := range seriesList {
+ seriesList[index].Label.Show = true
+ seriesList[index].RoundRadius = 5
+ }
+ _, err := NewBarChart(p, BarChartOption{
+ Padding: Box{
+ Left: 10,
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ },
+ SeriesList: seriesList,
+ XAxis: NewXAxisOption([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ YAxisOptions: NewYAxisOptions([]string{
+ "Rainfall",
+ "Evaporation",
+ }),
+ }).Render()
+ if err != nil {
+ return nil, err
+ }
+ return p.Bytes()
+ },
+ result: "",
+ },
+ }
+
+ 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))
+ }
+}
diff --git a/bar_series.go b/bar_series.go
deleted file mode 100644
index b730099..0000000
--- a/bar_series.go
+++ /dev/null
@@ -1,137 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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/wcharczuk/go-chart/v2"
-)
-
-const defaultBarMargin = 10
-
-type BarSeriesCustomStyle struct {
- PointIndex int
- Index int
- Style chart.Style
-}
-
-type BarSeries struct {
- BaseSeries
- Count int
- Index int
- // 间隔
- Margin int
- // 偏移量
- Offset int
- // 宽度
- BarWidth int
- CustomStyles []BarSeriesCustomStyle
-}
-
-type barSeriesWidthValues struct {
- columnWidth int
- columnMargin int
- margin int
- barWidth int
-}
-
-func (bs BarSeries) GetBarStyle(index, pointIndex int) chart.Style {
- // 指定样式
- for _, item := range bs.CustomStyles {
- if item.Index == index && item.PointIndex == pointIndex {
- return item.Style
- }
- }
- // 其它非指定样式
- return chart.Style{}
-}
-
-func (bs BarSeries) getWidthValues(width int) barSeriesWidthValues {
- columnWidth := width / bs.Len()
- // 块间隔
- columnMargin := columnWidth / 10
- minColumnMargin := 2
- if columnMargin < minColumnMargin {
- columnMargin = minColumnMargin
- }
- margin := bs.Margin
- if margin <= 0 {
- margin = defaultBarMargin
- }
- // 如果margin大于column margin
- if margin > columnMargin {
- margin = columnMargin
- }
-
- allBarMarginWidth := (bs.Count - 1) * margin
- barWidth := ((columnWidth - 2*columnMargin) - allBarMarginWidth) / bs.Count
- if bs.BarWidth > 0 && bs.BarWidth < barWidth {
- barWidth = bs.BarWidth
- // 重新计息columnMargin
- columnMargin = (columnWidth - allBarMarginWidth - (bs.Count * barWidth)) / 2
- }
- return barSeriesWidthValues{
- columnWidth: columnWidth,
- columnMargin: columnMargin,
- margin: margin,
- barWidth: barWidth,
- }
-}
-
-func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
- if bs.Len() == 0 || bs.Count <= 0 {
- return
- }
- style := bs.Style.InheritFrom(defaults)
- style.FillColor = style.StrokeColor
- if !style.ShouldDrawStroke() {
- return
- }
-
- cb := canvasBox.Bottom
- cl := canvasBox.Left
- widthValues := bs.getWidthValues(canvasBox.Width())
-
- for i := 0; i < bs.Len(); i++ {
- vx, vy := bs.GetValues(i)
- customStyle := bs.GetBarStyle(bs.Index, i)
- cloneStyle := style
- if !customStyle.IsZero() {
- cloneStyle.FillColor = customStyle.FillColor
- cloneStyle.StrokeColor = customStyle.StrokeColor
- }
-
- x := cl + xrange.Translate(vx)
- // 由于bar是居中展示,因此需要往前移一个显示块
- x += (-widthValues.columnWidth + widthValues.columnMargin)
- // 计算是第几个bar,位置右偏
- x += bs.Index * (widthValues.margin + widthValues.barWidth)
- y := cb - yrange.Translate(vy)
-
- chart.Draw.Box(r, chart.Box{
- Left: x,
- Top: y,
- Right: x + widthValues.barWidth,
- Bottom: canvasBox.Bottom - 1,
- }, cloneStyle)
- }
-}
diff --git a/bar_series_test.go b/bar_series_test.go
deleted file mode 100644
index 8703367..0000000
--- a/bar_series_test.go
+++ /dev/null
@@ -1,166 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/drawing"
-)
-
-func TestBarSeries(t *testing.T) {
- assert := assert.New(t)
-
- customStyle := chart.Style{
- StrokeColor: drawing.ColorBlue,
- }
- bs := BarSeries{
- CustomStyles: []BarSeriesCustomStyle{
- {
- PointIndex: 1,
- Style: customStyle,
- },
- },
- }
-
- assert.Equal(customStyle, bs.GetBarStyle(0, 1))
-
- assert.True(bs.GetBarStyle(1, 0).IsZero())
-}
-
-func TestBarSeriesGetWidthValues(t *testing.T) {
- assert := assert.New(t)
-
- bs := BarSeries{
- Count: 1,
- BaseSeries: BaseSeries{
- XValues: []float64{
- 1,
- 2,
- 3,
- },
- },
- }
- widthValues := bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 10,
- margin: 10,
- barWidth: 80,
- }, widthValues)
-
- // 指定margin
- bs.Margin = 5
- widthValues = bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 10,
- margin: 5,
- barWidth: 80,
- }, widthValues)
-
- // 指定bar的宽度
- bs.BarWidth = 60
- widthValues = bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 20,
- margin: 5,
- barWidth: 60,
- }, widthValues)
-}
-
-func TestBarSeriesRender(t *testing.T) {
- assert := assert.New(t)
-
- width := 800
- height := 400
-
- r, err := chart.SVG(width, height)
- assert.Nil(err)
-
- bs := BarSeries{
- Count: 1,
- CustomStyles: []BarSeriesCustomStyle{
- {
- Index: 0,
- PointIndex: 1,
- Style: chart.Style{
- StrokeColor: SeriesColorsLight[1],
- },
- },
- },
- BaseSeries: BaseSeries{
- TickPosition: chart.TickPositionBetweenTicks,
- Style: chart.Style{
- StrokeColor: SeriesColorsLight[0],
- StrokeWidth: 1,
- },
- XValues: []float64{
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- },
- YValues: []float64{
- // 第一个点为占位点
- 0,
- 120,
- 200,
- 150,
- 80,
- 70,
- 110,
- 130,
- },
- },
- }
- xrange := &chart.ContinuousRange{
- Min: 0,
- Max: 7,
- Domain: 753,
- }
- yrange := &chart.ContinuousRange{
- Min: 70,
- Max: 200,
- Domain: 362,
- }
- bs.Render(r, chart.Box{
- Top: 11,
- Left: 42,
- Right: 795,
- Bottom: 373,
- }, xrange, yrange, chart.Style{})
-
- buffer := bytes.Buffer{}
- err = r.Save(&buffer)
- assert.Nil(err)
- assert.Equal("", buffer.String())
-}
diff --git a/base_series.go b/base_series.go
deleted file mode 100644
index e17b4b9..0000000
--- a/base_series.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "fmt"
-
- "github.com/wcharczuk/go-chart/v2"
-)
-
-// Interface Assertions.
-var (
- _ chart.Series = (*BaseSeries)(nil)
- _ chart.FirstValuesProvider = (*BaseSeries)(nil)
- _ chart.LastValuesProvider = (*BaseSeries)(nil)
-)
-
-// BaseSeries represents a line on a chart.
-type BaseSeries struct {
- Name string
- Style chart.Style
- TickPosition chart.TickPosition
-
- YAxis chart.YAxisType
-
- XValueFormatter chart.ValueFormatter
- YValueFormatter chart.ValueFormatter
-
- XValues []float64
- YValues []float64
-}
-
-// GetName returns the name of the time series.
-func (bs BaseSeries) GetName() string {
- return bs.Name
-}
-
-// GetStyle returns the line style.
-func (bs BaseSeries) GetStyle() chart.Style {
- return bs.Style
-}
-
-// Len returns the number of elements in the series.
-func (bs BaseSeries) Len() int {
- offset := 0
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- offset = -1
- }
- return len(bs.XValues) + offset
-}
-
-// GetValues gets the x,y values at a given index.
-func (bs BaseSeries) GetValues(index int) (float64, float64) {
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- index++
- }
- return bs.XValues[index], bs.YValues[index]
-}
-
-// GetFirstValues gets the first x,y values.
-func (bs BaseSeries) GetFirstValues() (float64, float64) {
- index := 0
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- index++
- }
- return bs.XValues[index], bs.YValues[index]
-}
-
-// GetLastValues gets the last x,y values.
-func (bs BaseSeries) GetLastValues() (float64, float64) {
- return bs.XValues[len(bs.XValues)-1], bs.YValues[len(bs.YValues)-1]
-}
-
-// GetValueFormatters returns value formatter defaults for the series.
-func (bs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) {
- if bs.XValueFormatter != nil {
- x = bs.XValueFormatter
- } else {
- x = chart.FloatValueFormatter
- }
- if bs.YValueFormatter != nil {
- y = bs.YValueFormatter
- } else {
- y = chart.FloatValueFormatter
- }
- return
-}
-
-// GetYAxis returns which YAxis the series draws on.
-func (bs BaseSeries) GetYAxis() chart.YAxisType {
- return bs.YAxis
-}
-
-// Render renders the series.
-func (bs BaseSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
- fmt.Println("should be override the function")
-}
-
-// Validate validates the series.
-func (bs BaseSeries) Validate() error {
- if len(bs.XValues) == 0 {
- return fmt.Errorf("continuous series; must have xvalues set")
- }
-
- if len(bs.YValues) == 0 {
- return fmt.Errorf("continuous series; must have yvalues set")
- }
-
- if len(bs.XValues) != len(bs.YValues) {
- return fmt.Errorf("continuous series; must have same length xvalues as yvalues")
- }
- return nil
-}
diff --git a/base_series_test.go b/base_series_test.go
deleted file mode 100644
index 0c9b5d1..0000000
--- a/base_series_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "reflect"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
-)
-
-func TestBaseSeries(t *testing.T) {
- assert := assert.New(t)
-
- bs := BaseSeries{
- XValues: []float64{
- 1,
- 2,
- 3,
- },
- YValues: []float64{
- 10,
- 20,
- 30,
- },
- }
- assert.Equal(3, bs.Len())
- bs.TickPosition = chart.TickPositionBetweenTicks
- assert.Equal(2, bs.Len())
-
- bs.TickPosition = chart.TickPositionUnset
- x, y := bs.GetValues(1)
- assert.Equal(float64(2), x)
- assert.Equal(float64(20), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetValues(1)
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
-
- bs.TickPosition = chart.TickPositionUnset
- x, y = bs.GetFirstValues()
- assert.Equal(float64(1), x)
- assert.Equal(float64(10), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetFirstValues()
- assert.Equal(float64(2), x)
- assert.Equal(float64(20), y)
-
- bs.TickPosition = chart.TickPositionUnset
- x, y = bs.GetLastValues()
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetLastValues()
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
-
- xFormater, yFormater := bs.GetValueFormatters()
- assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(xFormater).Pointer())
- assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(yFormater).Pointer())
- formater := func(v interface{}) string {
- return ""
- }
- bs.XValueFormatter = formater
- bs.YValueFormatter = formater
- xFormater, yFormater = bs.GetValueFormatters()
- assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(xFormater).Pointer())
- assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(yFormater).Pointer())
-
- assert.Equal(chart.YAxisPrimary, bs.GetYAxis())
-
- assert.Nil(bs.Validate())
-}
diff --git a/chart_option.go b/chart_option.go
new file mode 100644
index 0000000..d80a383
--- /dev/null
+++ b/chart_option.go
@@ -0,0 +1,426 @@
+// 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 (
+ "sort"
+
+ "github.com/golang/freetype/truetype"
+)
+
+type ChartOption struct {
+ theme ColorPalette
+ font *truetype.Font
+ // The output type of chart, "svg" or "png", default value is "svg"
+ Type string
+ // The font family, which should be installed first
+ FontFamily string
+ // The theme of chart, "light" and "dark".
+ // The default theme is "light"
+ Theme string
+ // The title option
+ Title TitleOption
+ // The legend option
+ Legend LegendOption
+ // The x axis option
+ XAxis XAxisOption
+ // The y axis option list
+ YAxisOptions []YAxisOption
+ // The width of chart, default width is 600
+ Width int
+ // The height of chart, default height is 400
+ Height int
+ Parent *Painter
+ // The padding for chart, default padding is [20, 10, 10, 10]
+ Padding Box
+ // The canvas box for chart
+ Box Box
+ // The series list
+ SeriesList SeriesList
+ // The radar indicator list
+ 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
+type OptionFunc func(opt *ChartOption)
+
+// SVGTypeOption set svg type of chart's output
+func SVGTypeOption() OptionFunc {
+ return TypeOptionFunc(ChartOutputSVG)
+}
+
+// PNGTypeOption set png type of chart's output
+func PNGTypeOption() OptionFunc {
+ return TypeOptionFunc(ChartOutputPNG)
+}
+
+// TypeOptionFunc set type of chart's output
+func TypeOptionFunc(t string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Type = t
+ }
+}
+
+// FontFamilyOptionFunc set font family of chart
+func FontFamilyOptionFunc(fontFamily string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.FontFamily = fontFamily
+ }
+}
+
+// ThemeOptionFunc set them of chart
+func ThemeOptionFunc(theme string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Theme = theme
+ }
+}
+
+// TitleOptionFunc set title of chart
+func TitleOptionFunc(title TitleOption) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Title = title
+ }
+}
+
+// TitleTextOptionFunc set title text of chart
+func TitleTextOptionFunc(text string, subtext ...string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Title.Text = text
+ if len(subtext) != 0 {
+ opt.Title.Subtext = subtext[0]
+ }
+ }
+}
+
+// LegendOptionFunc set legend of chart
+func LegendOptionFunc(legend LegendOption) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Legend = legend
+ }
+}
+
+// LegendLabelsOptionFunc set legend labels of chart
+func LegendLabelsOptionFunc(labels []string, left ...string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Legend = NewLegendOption(labels, left...)
+ }
+}
+
+// XAxisOptionFunc set x axis of chart
+func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.XAxis = xAxisOption
+ }
+}
+
+// XAxisDataOptionFunc set x axis data of chart
+func XAxisDataOptionFunc(data []string, boundaryGap ...*bool) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.XAxis = NewXAxisOption(data, boundaryGap...)
+ }
+}
+
+// YAxisOptionFunc set y axis of chart, support two y axis
+func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.YAxisOptions = yAxisOption
+ }
+}
+
+// YAxisDataOptionFunc set y axis data of chart
+func YAxisDataOptionFunc(data []string) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.YAxisOptions = NewYAxisOptions(data)
+ }
+}
+
+// WidthOptionFunc set width of chart
+func WidthOptionFunc(width int) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Width = width
+ }
+}
+
+// HeightOptionFunc set height of chart
+func HeightOptionFunc(height int) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Height = height
+ }
+}
+
+// PaddingOptionFunc set padding of chart
+func PaddingOptionFunc(padding Box) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Padding = padding
+ }
+}
+
+// BoxOptionFunc set box of chart
+func BoxOptionFunc(box Box) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.Box = box
+ }
+}
+
+// PieSeriesShowLabel set pie series show label
+func PieSeriesShowLabel() OptionFunc {
+ return func(opt *ChartOption) {
+ for index := range opt.SeriesList {
+ opt.SeriesList[index].Label.Show = true
+ }
+ }
+}
+
+// ChildOptionFunc add child chart
+func ChildOptionFunc(child ...ChartOption) OptionFunc {
+ return func(opt *ChartOption) {
+ if opt.Children == nil {
+ opt.Children = make([]ChartOption, 0)
+ }
+ opt.Children = append(opt.Children, child...)
+ }
+}
+
+// RadarIndicatorOptionFunc set radar indicator of chart
+func RadarIndicatorOptionFunc(names []string, values []float64) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.RadarIndicators = NewRadarIndicators(names, values)
+ }
+}
+
+// BackgroundColorOptionFunc set background color of chart
+func BackgroundColorOptionFunc(color Color) OptionFunc {
+ return func(opt *ChartOption) {
+ opt.BackgroundColor = color
+ }
+}
+
+// MarkLineOptionFunc set mark line for series of chart
+func MarkLineOptionFunc(seriesIndex int, markLineTypes ...string) OptionFunc {
+ return func(opt *ChartOption) {
+ if len(opt.SeriesList) <= seriesIndex {
+ return
+ }
+ opt.SeriesList[seriesIndex].MarkLine = NewMarkLine(markLineTypes...)
+ }
+}
+
+// MarkPointOptionFunc set mark point for series of chart
+func MarkPointOptionFunc(seriesIndex int, markPointTypes ...string) OptionFunc {
+ return func(opt *ChartOption) {
+ if len(opt.SeriesList) <= seriesIndex {
+ return
+ }
+ opt.SeriesList[seriesIndex].MarkPoint = NewMarkPoint(markPointTypes...)
+ }
+}
+
+func (o *ChartOption) fillDefault() {
+ t := NewTheme(o.Theme)
+ o.theme = t
+ // 如果为空,初始化
+ axisCount := 1
+ for _, series := range o.SeriesList {
+ if series.AxisIndex >= axisCount {
+ axisCount++
+ }
+ }
+ o.Width = getDefaultInt(o.Width, defaultChartWidth)
+ o.Height = getDefaultInt(o.Height, defaultChartHeight)
+ yAxisOptions := make([]YAxisOption, axisCount)
+ copy(yAxisOptions, o.YAxisOptions)
+ o.YAxisOptions = yAxisOptions
+ o.font, _ = GetFont(o.FontFamily)
+
+ if o.font == nil {
+ o.font, _ = GetDefaultFont()
+ } else {
+ // 如果指定了字体,则设置主题的字体
+ t.SetFont(o.font)
+ }
+ if o.BackgroundColor.IsZero() {
+ o.BackgroundColor = t.GetBackgroundColor()
+ }
+ if o.Padding.IsZero() {
+ o.Padding = Box{
+ Top: 20,
+ Right: 20,
+ Bottom: 20,
+ Left: 20,
+ }
+ }
+ // legend与series name的关联
+ if len(o.Legend.Data) == 0 {
+ o.Legend.Data = o.SeriesList.Names()
+ } else {
+ seriesCount := len(o.SeriesList)
+ for index, name := range o.Legend.Data {
+ if index < seriesCount &&
+ len(o.SeriesList[index].Name) == 0 {
+ o.SeriesList[index].Name = name
+ }
+ }
+ nameIndexDict := map[string]int{}
+ for index, name := range o.Legend.Data {
+ nameIndexDict[name] = index
+ }
+ // 保证series的顺序与legend一致
+ sort.Slice(o.SeriesList, func(i, j int) bool {
+ return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name]
+ })
+ }
+}
+
+// LineRender line chart render
+func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
+ seriesList := NewSeriesListDataFromValues(values, ChartTypeLine)
+ return Render(ChartOption{
+ SeriesList: seriesList,
+ }, opts...)
+}
+
+// BarRender bar chart render
+func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
+ seriesList := NewSeriesListDataFromValues(values, ChartTypeBar)
+ return Render(ChartOption{
+ SeriesList: seriesList,
+ }, opts...)
+}
+
+// HorizontalBarRender horizontal bar chart render
+func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
+ seriesList := NewSeriesListDataFromValues(values, ChartTypeHorizontalBar)
+ return Render(ChartOption{
+ SeriesList: seriesList,
+ }, opts...)
+}
+
+// PieRender pie chart render
+func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) {
+ return Render(ChartOption{
+ SeriesList: NewPieSeriesList(values),
+ }, opts...)
+}
+
+// RadarRender radar chart render
+func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
+ seriesList := NewSeriesListDataFromValues(values, ChartTypeRadar)
+ return Render(ChartOption{
+ SeriesList: seriesList,
+ }, opts...)
+}
+
+// FunnelRender funnel chart render
+func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) {
+ seriesList := NewFunnelSeriesList(values)
+ return Render(ChartOption{
+ 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
+}
diff --git a/chart_option_test.go b/chart_option_test.go
new file mode 100644
index 0000000..c354b26
--- /dev/null
+++ b/chart_option_test.go
@@ -0,0 +1,451 @@
+// 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"
+ "git.smarteching.com/zeni/go-chart/v2/drawing"
+)
+
+func TestChartOption(t *testing.T) {
+ assert := assert.New(t)
+
+ fns := []OptionFunc{
+ SVGTypeOption(),
+ FontFamilyOptionFunc("fontFamily"),
+ ThemeOptionFunc("theme"),
+ TitleTextOptionFunc("title"),
+ LegendLabelsOptionFunc([]string{
+ "label",
+ }),
+ XAxisDataOptionFunc([]string{
+ "xaxis",
+ }),
+ YAxisDataOptionFunc([]string{
+ "yaxis",
+ }),
+ WidthOptionFunc(800),
+ HeightOptionFunc(600),
+ PaddingOptionFunc(Box{
+ Left: 10,
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ }),
+ BackgroundColorOptionFunc(drawing.ColorBlack),
+ }
+ opt := ChartOption{}
+ for _, fn := range fns {
+ fn(&opt)
+ }
+ assert.Equal(ChartOption{
+ Type: ChartOutputSVG,
+ FontFamily: "fontFamily",
+ Theme: "theme",
+ Title: TitleOption{
+ Text: "title",
+ },
+ Legend: LegendOption{
+ Data: []string{
+ "label",
+ },
+ },
+ XAxis: XAxisOption{
+ Data: []string{
+ "xaxis",
+ },
+ },
+ YAxisOptions: []YAxisOption{
+ {
+ Data: []string{
+ "yaxis",
+ },
+ },
+ },
+ Width: 800,
+ Height: 600,
+ Padding: Box{
+ Left: 10,
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ },
+ BackgroundColor: drawing.ColorBlack,
+ }, opt)
+}
+
+func TestChartOptionPieSeriesShowLabel(t *testing.T) {
+ assert := assert.New(t)
+
+ opt := ChartOption{
+ SeriesList: NewPieSeriesList([]float64{
+ 1,
+ 2,
+ }),
+ }
+ PieSeriesShowLabel()(&opt)
+ assert.True(opt.SeriesList[0].Label.Show)
+}
+
+func TestChartOptionMarkLine(t *testing.T) {
+ assert := assert.New(t)
+ opt := ChartOption{
+ SeriesList: NewSeriesListDataFromValues([][]float64{
+ {1, 2},
+ }),
+ }
+ MarkLineOptionFunc(0, "min", "max")(&opt)
+ assert.Equal(NewMarkLine("min", "max"), opt.SeriesList[0].MarkLine)
+}
+
+func TestChartOptionMarkPoint(t *testing.T) {
+ assert := assert.New(t)
+ opt := ChartOption{
+ SeriesList: NewSeriesListDataFromValues([][]float64{
+ {1, 2},
+ }),
+ }
+ MarkPointOptionFunc(0, "min", "max")(&opt)
+ assert.Equal(NewMarkPoint("min", "max"), opt.SeriesList[0].MarkPoint)
+}
+
+func TestLineRender(t *testing.T) {
+ assert := assert.New(t)
+ values := [][]float64{
+ {
+ 120,
+ 132,
+ 101,
+ 134,
+ 90,
+ 230,
+ 210,
+ },
+ {
+ 220,
+ 182,
+ 191,
+ 234,
+ 290,
+ 330,
+ 310,
+ },
+ {
+ 150,
+ 232,
+ 201,
+ 154,
+ 190,
+ 330,
+ 410,
+ },
+ {
+ 320,
+ 332,
+ 301,
+ 334,
+ 390,
+ 330,
+ 320,
+ },
+ {
+ 820,
+ 932,
+ 901,
+ 934,
+ 1290,
+ 1330,
+ 1320,
+ },
+ }
+ p, err := LineRender(
+ values,
+ SVGTypeOption(),
+ TitleTextOptionFunc("Line"),
+ XAxisDataOptionFunc([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }),
+ LegendLabelsOptionFunc([]string{
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ "Direct",
+ "Search Engine",
+ }, PositionCenter),
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestBarRender(t *testing.T) {
+ assert := assert.New(t)
+ values := [][]float64{
+ {
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ },
+ {
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ },
+ }
+ p, err := BarRender(
+ values,
+ SVGTypeOption(),
+ XAxisDataOptionFunc([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ LegendLabelsOptionFunc([]string{
+ "Rainfall",
+ "Evaporation",
+ }, PositionRight),
+ MarkLineOptionFunc(0, SeriesMarkDataTypeAverage),
+ MarkPointOptionFunc(0, SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin),
+ // custom option func
+ func(opt *ChartOption) {
+ opt.SeriesList[1].MarkPoint = NewMarkPoint(
+ SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin,
+ )
+ opt.SeriesList[1].MarkLine = NewMarkLine(
+ SeriesMarkDataTypeAverage,
+ )
+ },
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestHorizontalBarRender(t *testing.T) {
+ assert := assert.New(t)
+ values := [][]float64{
+ {
+ 18203,
+ 23489,
+ 29034,
+ 104970,
+ 131744,
+ 630230,
+ },
+ {
+ 19325,
+ 23438,
+ 31000,
+ 121594,
+ 134141,
+ 681807,
+ },
+ }
+ p, err := HorizontalBarRender(
+ values,
+ SVGTypeOption(),
+ TitleTextOptionFunc("World Population"),
+ PaddingOptionFunc(Box{
+ Top: 20,
+ Right: 40,
+ Bottom: 20,
+ Left: 20,
+ }),
+ LegendLabelsOptionFunc([]string{
+ "2011",
+ "2012",
+ }),
+ YAxisDataOptionFunc([]string{
+ "Brazil",
+ "Indonesia",
+ "USA",
+ "India",
+ "China",
+ "World",
+ }),
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestPieRender(t *testing.T) {
+ assert := assert.New(t)
+ values := []float64{
+ 1048,
+ 735,
+ 580,
+ 484,
+ 300,
+ }
+ p, err := PieRender(
+ values,
+ SVGTypeOption(),
+ TitleOptionFunc(TitleOption{
+ Text: "Rainfall vs Evaporation",
+ Subtext: "Fake Data",
+ Left: PositionCenter,
+ }),
+ PaddingOptionFunc(Box{
+ Top: 20,
+ Right: 20,
+ Bottom: 20,
+ Left: 20,
+ }),
+ LegendOptionFunc(LegendOption{
+ Orient: OrientVertical,
+ Data: []string{
+ "Search Engine",
+ "Direct",
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ },
+ Left: PositionLeft,
+ }),
+ PieSeriesShowLabel(),
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestRadarRender(t *testing.T) {
+ assert := assert.New(t)
+
+ values := [][]float64{
+ {
+ 4200,
+ 3000,
+ 20000,
+ 35000,
+ 50000,
+ 18000,
+ },
+ {
+ 5000,
+ 14000,
+ 28000,
+ 26000,
+ 42000,
+ 21000,
+ },
+ }
+ p, err := RadarRender(
+ values,
+ SVGTypeOption(),
+ TitleTextOptionFunc("Basic Radar Chart"),
+ LegendLabelsOptionFunc([]string{
+ "Allocated Budget",
+ "Actual Spending",
+ }),
+ RadarIndicatorOptionFunc([]string{
+ "Sales",
+ "Administration",
+ "Information Technology",
+ "Customer Support",
+ "Development",
+ "Marketing",
+ }, []float64{
+ 6500,
+ 16000,
+ 30000,
+ 38000,
+ 52000,
+ 25000,
+ }),
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestFunnelRender(t *testing.T) {
+ assert := assert.New(t)
+
+ values := []float64{
+ 100,
+ 80,
+ 60,
+ 40,
+ 20,
+ }
+ p, err := FunnelRender(
+ values,
+ SVGTypeOption(),
+ TitleTextOptionFunc("Funnel"),
+ LegendLabelsOptionFunc([]string{
+ "Show",
+ "Click",
+ "Visit",
+ "Inquiry",
+ "Order",
+ }),
+ )
+ assert.Nil(err)
+ data, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/charts.go b/charts.go
index f5fcb1f..31df11c 100644
--- a/charts.go
+++ b/charts.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,202 +23,451 @@
package charts
import (
- "bytes"
"errors"
- "io"
+ "math"
+ "sort"
- "github.com/wcharczuk/go-chart/v2"
+ "git.smarteching.com/zeni/go-chart/v2"
)
-const (
- ThemeLight = "light"
- ThemeDark = "dark"
-)
+const labelFontSize = 10
+const smallLabelFontSize = 8
+const defaultDotWidth = 2.0
+const defaultStrokeWidth = 2.0
-const (
- DefaultChartWidth = 800
- DefaultChartHeight = 400
-)
+var defaultChartWidth = 600
+var defaultChartHeight = 400
-type (
- Title struct {
- Text string
- Style chart.Style
+// SetDefaultWidth sets default width of chart
+func SetDefaultWidth(width int) {
+ if width > 0 {
+ defaultChartWidth = width
}
- Legend struct {
- Data []string
- Align string
- Padding chart.Box
- Left string
- Right string
- Top string
- Bottom string
- }
- Options struct {
- Padding chart.Box
- Width int
- Height int
- Theme string
- XAxis XAxis
- YAxisOptions []*YAxisOption
- Series []Series
- Title Title
- Legend Legend
- TickPosition chart.TickPosition
- Log chart.Logger
- }
-)
-
-type Graph interface {
- Render(rp chart.RendererProvider, w io.Writer) error
}
-func (o *Options) validate() error {
- if len(o.Series) == 0 {
- return errors.New("series can not be empty")
+// SetDefaultHeight sets default height of chart
+func SetDefaultHeight(height int) {
+ if height > 0 {
+ defaultChartHeight = height
}
- xAxisCount := len(o.XAxis.Data)
+}
- for _, item := range o.Series {
- if item.Type != SeriesPie && len(item.Data) != xAxisCount {
- return errors.New("series and xAxis is not matched")
+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)
+}
+
+type renderHandler struct {
+ list []func() error
+}
+
+func (rh *renderHandler) Add(fn func() error) {
+ list := rh.list
+ if len(list) == 0 {
+ list = make([]func() error, 0)
+ }
+ rh.list = append(list, fn)
+}
+
+func (rh *renderHandler) Do() error {
+ for _, fn := range rh.list {
+ err := fn()
+ if err != nil {
+ return err
}
}
return nil
}
-func (o *Options) getWidth() int {
- width := o.Width
- if width <= 0 {
- width = DefaultChartWidth
- }
- return width
+type defaultRenderOption struct {
+ Theme ColorPalette
+ Padding Box
+ SeriesList SeriesList
+ // The y axis option
+ YAxisOptions []YAxisOption
+ // The x axis option
+ XAxis XAxisOption
+ // The title option
+ TitleOption TitleOption
+ // The legend option
+ LegendOption LegendOption
+ // background is filled
+ backgroundIsFilled bool
+ // x y axis is reversed
+ axisReversed bool
}
-func (o *Options) getHeight() int {
- height := o.Height
- if height <= 0 {
- height = DefaultChartHeight
- }
- return height
+type defaultRenderResult struct {
+ axisRanges map[int]axisRange
+ // 图例区域
+ seriesPainter *Painter
}
-func (o *Options) getBackground() chart.Style {
- bg := chart.Style{
- Padding: o.Padding,
+func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) {
+ seriesList := opt.SeriesList
+ seriesList.init()
+ if !opt.backgroundIsFilled {
+ p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor())
}
- return bg
-}
-func render(g Graph, rp chart.RendererProvider) ([]byte, error) {
- buf := bytes.Buffer{}
- err := g.Render(rp, &buf)
+ if !opt.Padding.IsZero() {
+ 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()
+ if err != nil {
+ return nil, err
+ }
+ legendHeight = legendResult.Height()
+ }
+
+ // 如果有标题
+ if opt.TitleOption.Text != "" {
+ if opt.TitleOption.Theme == nil {
+ opt.TitleOption.Theme = opt.Theme
+ }
+ titlePainter := NewTitlePainter(p, opt.TitleOption)
+
+ titleBox, err := titlePainter.Render()
+ 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,
+ }))
+ }
+
+ result := defaultRenderResult{
+ axisRanges: make(map[int]axisRange),
+ }
+
+ // 计算图表对应的轴有哪些
+ axisIndexList := make([]int, 0)
+ for _, series := range opt.SeriesList {
+ if containsInt(axisIndexList, series.AxisIndex) {
+ continue
+ }
+ axisIndexList = append(axisIndexList, series.AxisIndex)
+ }
+ // 高度需要减去x轴的高度
+ rangeHeight := p.Height() - defaultXAxisHeight
+ rangeWidthLeft := 0
+ rangeWidthRight := 0
+
+ // 倒序
+ sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList)))
+
+ // 计算对应的axis range
+ for _, index := range axisIndexList {
+ yAxisOption := YAxisOption{}
+ if len(opt.YAxisOptions) > index {
+ yAxisOption = opt.YAxisOptions[index]
+ }
+ divideCount := yAxisOption.DivideCount
+ if divideCount <= 0 {
+ divideCount = defaultAxisDivideCount
+ }
+ max, min := opt.SeriesList.GetMaxMin(index)
+ r := NewRange(AxisRangeOption{
+ Painter: p,
+ Min: min,
+ Max: max,
+ // 高度需要减去x轴的高度
+ Size: rangeHeight,
+ // 分隔数量
+ DivideCount: divideCount,
+ })
+ if yAxisOption.Min != nil && *yAxisOption.Min <= min {
+ r.min = *yAxisOption.Min
+ }
+ if yAxisOption.Max != nil && *yAxisOption.Max >= max {
+ r.max = *yAxisOption.Max
+ }
+ result.axisRanges[index] = r
+
+ if yAxisOption.Theme == nil {
+ yAxisOption.Theme = opt.Theme
+ }
+ if !opt.axisReversed {
+ 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.isValueAxis = true
+ }
+ reverseStringSlice(yAxisOption.Data)
+ // TODO生成其它位置既yAxis
+ var yAxis *axisPainter
+ child := p.Child(PainterPaddingOption(Box{
+ Left: rangeWidthLeft,
+ Right: rangeWidthRight,
+ }))
+ if index == 0 {
+ yAxis = NewLeftYAxis(child, yAxisOption)
+ } else {
+ yAxis = NewRightYAxis(child, yAxisOption)
+ }
+ yAxisBox, err := yAxis.Render()
+ if err != nil {
+ return nil, err
+ }
+ if index == 0 {
+ rangeWidthLeft += yAxisBox.Width()
+ } else {
+ rangeWidthRight += yAxisBox.Width()
+ }
+ }
+
+ if opt.XAxis.Theme == nil {
+ opt.XAxis.Theme = opt.Theme
+ }
+ xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
+ Left: rangeWidthLeft,
+ Right: rangeWidthRight,
+ })), opt.XAxis)
+ _, err := xAxis.Render()
if err != nil {
return nil, err
}
- return buf.Bytes(), nil
+
+ result.seriesPainter = p.Child(PainterPaddingOption(Box{
+ Bottom: defaultXAxisHeight,
+ Left: rangeWidthLeft,
+ Right: rangeWidthRight,
+ }))
+ return &result, nil
}
-func ToPNG(g Graph) ([]byte, error) {
- return render(g, chart.PNG)
-}
-
-func ToSVG(g Graph) ([]byte, error) {
- return render(g, chart.SVG)
-}
-
-func newPieChart(opt Options) *chart.PieChart {
- values := make(chart.Values, len(opt.Series))
- for index, item := range opt.Series {
- values[index] = chart.Value{
- Value: item.Data[0].Value,
- Label: item.Name,
+func doRender(renderers ...Renderer) error {
+ for _, r := range renderers {
+ _, err := r.Render()
+ if err != nil {
+ return err
}
}
- return &chart.PieChart{
- Background: opt.getBackground(),
- Title: opt.Title.Text,
- TitleStyle: opt.Title.Style,
- Width: opt.getWidth(),
- Height: opt.getHeight(),
- Values: values,
- ColorPalette: &PieThemeColorPalette{
- ThemeColorPalette: ThemeColorPalette{
- Theme: opt.Theme,
+ return nil
+}
+
+func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
+ for _, fn := range opts {
+ fn(&opt)
+ }
+ opt.fillDefault()
+
+ isChild := true
+ if opt.Parent == nil {
+ isChild = false
+ p, err := NewPainter(PainterOptions{
+ Type: opt.Type,
+ Width: opt.Width,
+ Height: opt.Height,
+ Font: opt.font,
+ })
+ if err != nil {
+ return nil, err
+ }
+ opt.Parent = p
+ }
+ p := opt.Parent
+ if opt.ValueFormatter != nil {
+ p.valueFormatter = opt.ValueFormatter
+ }
+ if !opt.Box.IsZero() {
+ p = p.Child(PainterBoxOption(opt.Box))
+ }
+ if !isChild {
+ p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
+ }
+ seriesList := opt.SeriesList
+ seriesList.init()
+
+ seriesCount := len(seriesList)
+
+ // line chart
+ lineSeriesList := seriesList.Filter(ChartTypeLine)
+ barSeriesList := seriesList.Filter(ChartTypeBar)
+ horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar)
+ pieSeriesList := seriesList.Filter(ChartTypePie)
+ radarSeriesList := seriesList.Filter(ChartTypeRadar)
+ funnelSeriesList := seriesList.Filter(ChartTypeFunnel)
+
+ if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount {
+ return nil, errors.New("Horizontal bar can not mix other charts")
+ }
+ if len(pieSeriesList) != 0 && len(pieSeriesList) != seriesCount {
+ return nil, errors.New("Pie can not mix other charts")
+ }
+ if len(radarSeriesList) != 0 && len(radarSeriesList) != seriesCount {
+ return nil, errors.New("Radar can not mix other charts")
+ }
+ if len(funnelSeriesList) != 0 && len(funnelSeriesList) != seriesCount {
+ return nil, errors.New("Funnel can not mix other charts")
+ }
+
+ axisReversed := len(horizontalBarSeriesList) != 0
+ renderOpt := defaultRenderOption{
+ Theme: opt.theme,
+ Padding: opt.Padding,
+ SeriesList: opt.SeriesList,
+ XAxis: opt.XAxis,
+ YAxisOptions: opt.YAxisOptions,
+ TitleOption: opt.Title,
+ LegendOption: opt.Legend,
+ axisReversed: axisReversed,
+ // 前置已设置背景色
+ backgroundIsFilled: true,
+ }
+ if len(pieSeriesList) != 0 ||
+ len(radarSeriesList) != 0 ||
+ len(funnelSeriesList) != 0 {
+ renderOpt.XAxis.Show = FalseFlag()
+ renderOpt.YAxisOptions = []YAxisOption{
+ {
+ Show: FalseFlag(),
},
- },
- }
-}
-
-func newChart(opt Options) *chart.Chart {
- tickPosition := opt.TickPosition
-
- xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme)
-
- legendSize := len(opt.Legend.Data)
- for index, item := range opt.Series {
- if len(item.XValues) == 0 {
- opt.Series[index].XValues = xValues
- }
- if index < legendSize && opt.Series[index].Name == "" {
- opt.Series[index].Name = opt.Legend.Data[index]
}
}
-
- var secondaryYAxisOption *YAxisOption
- if len(opt.YAxisOptions) != 0 {
- secondaryYAxisOption = opt.YAxisOptions[0]
+ if len(horizontalBarSeriesList) != 0 {
+ renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data)
+ renderOpt.YAxisOptions[0].Unit = 1
}
- yAxisOption := &YAxisOption{
- Disabled: true,
- }
- if len(opt.YAxisOptions) > 1 {
- yAxisOption = opt.YAxisOptions[1]
- }
-
- c := &chart.Chart{
- Log: opt.Log,
- Background: opt.getBackground(),
- ColorPalette: &ThemeColorPalette{
- Theme: opt.Theme,
- },
- Title: opt.Title.Text,
- TitleStyle: opt.Title.Style,
- Width: opt.getWidth(),
- Height: opt.getHeight(),
- XAxis: xAxis,
- YAxis: GetYAxis(opt.Theme, yAxisOption),
- YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption),
- Series: GetSeries(opt.Series, tickPosition, opt.Theme),
- }
-
- // 设置secondary的样式
- if legendSize != 0 {
- c.Elements = []chart.Renderable{
- LegendCustomize(c.Series, LegendOption{
- Theme: opt.Theme,
- IconDraw: DefaultLegendIconDraw,
- Align: opt.Legend.Align,
- Padding: opt.Legend.Padding,
- Left: opt.Legend.Left,
- Right: opt.Legend.Right,
- Top: opt.Legend.Top,
- Bottom: opt.Legend.Bottom,
- }),
- }
- }
- return c
-}
-
-func New(opt Options) (Graph, error) {
- err := opt.validate()
+ renderResult, err := defaultRender(p, renderOpt)
if err != nil {
return nil, err
}
- if opt.Series[0].Type == SeriesPie {
- return newPieChart(opt), nil
+
+ handler := renderHandler{}
+
+ // bar chart
+ 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,
+ }).render(renderResult, barSeriesList)
+ return err
+ })
}
- return newChart(opt), nil
+ // horizontal bar chart
+ if len(horizontalBarSeriesList) != 0 {
+ handler.Add(func() 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
+ })
+ }
+
+ // pie chart
+ if len(pieSeriesList) != 0 {
+ handler.Add(func() error {
+ _, err := NewPieChart(p, PieChartOption{
+ Theme: opt.theme,
+ Font: opt.font,
+ }).render(renderResult, pieSeriesList)
+ return err
+ })
+ }
+
+ // line chart
+ 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,
+ }).render(renderResult, lineSeriesList)
+ return err
+ })
+ }
+
+ // radar chart
+ if len(radarSeriesList) != 0 {
+ handler.Add(func() error {
+ _, err := NewRadarChart(p, RadarChartOption{
+ Theme: opt.theme,
+ Font: opt.font,
+ // 相应值
+ RadarIndicators: opt.RadarIndicators,
+ }).render(renderResult, radarSeriesList)
+ return err
+ })
+ }
+
+ // funnel chart
+ if len(funnelSeriesList) != 0 {
+ handler.Add(func() error {
+ _, err := NewFunnelChart(p, FunnelChartOption{
+ Theme: opt.theme,
+ Font: opt.font,
+ }).render(renderResult, funnelSeriesList)
+ return err
+ })
+ }
+
+ err = handler.Do()
+
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range opt.Children {
+ item.Parent = p
+ if item.Theme == "" {
+ item.Theme = opt.Theme
+ }
+ if item.FontFamily == "" {
+ item.FontFamily = opt.FontFamily
+ }
+ _, err = Render(item)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return p, nil
}
diff --git a/charts_test.go b/charts_test.go
index 3173868..bd581e9 100644
--- a/charts_test.go
+++ b/charts_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -26,118 +26,230 @@ import (
"errors"
"testing"
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
+ "git.smarteching.com/zeni/go-chart/v2"
)
-func TestChartsOptions(t *testing.T) {
- assert := assert.New(t)
-
- o := Options{}
-
- assert.Equal(errors.New("series can not be empty"), o.validate())
-
- o.Series = []Series{
- {
- Data: []SeriesData{
- {
- Value: 1,
+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",
},
},
- },
- }
- assert.Equal(errors.New("series and xAxis is not matched"), o.validate())
- o.XAxis.Data = []string{
- "1",
- }
- assert.Nil(o.validate())
-
- assert.Equal(DefaultChartWidth, o.getWidth())
- o.Width = 10
- assert.Equal(10, o.getWidth())
-
- assert.Equal(DefaultChartHeight, o.getHeight())
- o.Height = 10
- assert.Equal(10, o.getHeight())
-
- padding := chart.NewBox(10, 10, 10, 10)
- o.Padding = padding
- assert.Equal(padding, o.getBackground().Padding)
-}
-
-func TestNewPieChart(t *testing.T) {
- assert := assert.New(t)
-
- data := []Series{
- {
- Data: []SeriesData{
+ Padding: chart.Box{
+ Top: 100,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ },
+ XAxis: NewXAxisOption([]string{
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017",
+ }),
+ YAxisOptions: []YAxisOption{
{
- Value: 10,
+
+ Min: NewFloatPoint(0),
+ Max: NewFloatPoint(90),
},
},
- Name: "chrome",
- },
- {
- Data: []SeriesData{
+ 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{
{
- Value: 2,
+ 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%",
+ }),
},
},
- Name: "edge",
- },
- }
- pie := newPieChart(Options{
- Series: data,
- })
- for index, item := range pie.Values {
- assert.Equal(data[index].Name, item.Label)
- assert.Equal(data[index].Data[0].Value, item.Value)
+ }
+ 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 TestNewChart(t *testing.T) {
- assert := assert.New(t)
+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{
+ {
- data := []Series{
- {
- Data: []SeriesData{
- {
- Value: 10,
- },
- {
- Value: 20,
+ Min: NewFloatPoint(0),
+ Max: NewFloatPoint(90),
},
},
- Name: "chrome",
- },
- {
- Data: []SeriesData{
+ 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{
{
- Value: 2,
- },
- {
- Value: 3,
+ 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%",
+ }),
},
},
- Name: "edge",
- },
+ }
+ 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"))
+ }
}
-
- c := newChart(Options{
- Series: data,
- })
- assert.Empty(c.Elements)
- for index, series := range c.Series {
- assert.Equal(data[index].Name, series.GetName())
- }
-
- c = newChart(Options{
- Legend: Legend{
- Data: []string{
- "chrome",
- "edge",
- },
- },
- })
- assert.Equal(1, len(c.Elements))
}
diff --git a/echarts.go b/echarts.go
index bd5a950..aaef1f1 100644
--- a/echarts.go
+++ b/echarts.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -28,21 +28,10 @@ import (
"fmt"
"regexp"
"strconv"
- "strings"
- "github.com/wcharczuk/go-chart/v2"
+ "git.smarteching.com/zeni/go-chart/v2"
)
-type EChartStyle struct {
- Color string `json:"color"`
-}
-type ECharsSeriesData struct {
- Value float64 `json:"value"`
- Name string `json:"name"`
- ItemStyle EChartStyle `json:"itemStyle"`
-}
-type _ECharsSeriesData ECharsSeriesData
-
func convertToArray(data []byte) []byte {
data = bytes.TrimSpace(data)
if len(data) == 0 {
@@ -54,20 +43,79 @@ func convertToArray(data []byte) []byte {
return data
}
-func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
- data = bytes.TrimSpace(data)
+type EChartsPosition string
+
+func (p *EChartsPosition) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
if regexp.MustCompile(`^\d+`).Match(data) {
+ data = []byte(fmt.Sprintf(`"%s"`, string(data)))
+ }
+ s := (*string)(p)
+ return json.Unmarshal(data, s)
+}
+
+type EChartStyle struct {
+ Color string `json:"color"`
+}
+
+func (es *EChartStyle) ToStyle() Style {
+ color := parseColor(es.Color)
+ return Style{
+ FillColor: color,
+ FontColor: color,
+ StrokeColor: color,
+ }
+}
+
+type EChartsSeriesDataValue struct {
+ values []float64
+}
+
+func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error {
+ data = convertToArray(data)
+ return json.Unmarshal(data, &value.values)
+}
+func (value *EChartsSeriesDataValue) First() float64 {
+ if len(value.values) == 0 {
+ return 0
+ }
+ return value.values[0]
+}
+func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue {
+ return EChartsSeriesDataValue{
+ values: values,
+ }
+}
+
+type EChartsSeriesData struct {
+ Value EChartsSeriesDataValue `json:"value"`
+ Name string `json:"name"`
+ ItemStyle EChartStyle `json:"itemStyle"`
+}
+type _EChartsSeriesData EChartsSeriesData
+
+var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`)
+
+func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
+ data = bytes.TrimSpace(data)
+ if len(data) == 0 {
+ return nil
+ }
+ if numericRep.Match(data) {
v, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
}
- es.Value = v
+ es.Value = EChartsSeriesDataValue{
+ values: []float64{
+ v,
+ },
+ }
return nil
}
- v := _ECharsSeriesData{}
+ v := _EChartsSeriesData{}
err := json.Unmarshal(data, &v)
if err != nil {
return err
@@ -78,24 +126,55 @@ func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
return nil
}
-type EChartsPadding struct {
- box chart.Box
+type EChartsXAxisData struct {
+ BoundaryGap *bool `json:"boundaryGap"`
+ SplitNumber int `json:"splitNumber"`
+ Data []string `json:"data"`
+ Type string `json:"type"`
+}
+type EChartsXAxis struct {
+ Data []EChartsXAxisData
}
-type LegendPostion string
-
-func (lp *LegendPostion) UnmarshalJSON(data []byte) error {
+func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
+ data = convertToArray(data)
if len(data) == 0 {
return nil
}
- if regexp.MustCompile(`^\d+`).Match(data) {
- data = []byte(fmt.Sprintf(`"%s"`, string(data)))
- }
- s := (*string)(lp)
- return json.Unmarshal(data, s)
+ return json.Unmarshal(data, &ex.Data)
}
-func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
+type EChartsAxisLabel struct {
+ Formatter string `json:"formatter"`
+}
+type EChartsYAxisData struct {
+ Min *float64 `json:"min"`
+ Max *float64 `json:"max"`
+ AxisLabel EChartsAxisLabel `json:"axisLabel"`
+ AxisLine struct {
+ LineStyle struct {
+ Color string `json:"color"`
+ } `json:"lineStyle"`
+ } `json:"axisLine"`
+ Data []string `json:"data"`
+}
+type EChartsYAxis struct {
+ Data []EChartsYAxisData `json:"data"`
+}
+
+func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
+ data = convertToArray(data)
+ if len(data) == 0 {
+ return nil
+ }
+ return json.Unmarshal(data, &ey.Data)
+}
+
+type EChartsPadding struct {
+ Box chart.Box
+}
+
+func (eb *EChartsPadding) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
if len(data) == 0 {
return nil
@@ -110,14 +189,14 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
}
switch len(arr) {
case 1:
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Left: arr[0],
Top: arr[0],
Bottom: arr[0],
Right: arr[0],
}
case 2:
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Top: arr[0],
Bottom: arr[0],
Left: arr[1],
@@ -130,7 +209,7 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
result[3] = result[1]
}
// 上右下左
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Top: result[0],
Right: result[1],
Bottom: result[2],
@@ -140,236 +219,310 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
return nil
}
-type EChartsYAxis struct {
- Data []struct {
- Min *float64 `json:"min"`
- Max *float64 `json:"max"`
- // Interval int `json:"interval"`
- AxisLabel struct {
- Formatter string `json:"formatter"`
- } `json:"axisLabel"`
- } `json:"data"`
+type EChartsLabelOption struct {
+ Show bool `json:"show"`
+ Distance int `json:"distance"`
+ Color string `json:"color"`
+}
+type EChartsLegend struct {
+ Show *bool `json:"show"`
+ Data []string `json:"data"`
+ Align string `json:"align"`
+ Orient string `json:"orient"`
+ Padding EChartsPadding `json:"padding"`
+ Left EChartsPosition `json:"left"`
+ Top EChartsPosition `json:"top"`
+ TextStyle EChartsTextStyle `json:"textStyle"`
}
-func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
- data = convertToArray(data)
+type EChartsMarkData struct {
+ Type string `json:"type"`
+}
+type _EChartsMarkData EChartsMarkData
+
+func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error {
+ data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
- return json.Unmarshal(data, &ey.Data)
-}
-
-type EChartsXAxis struct {
- Data []struct {
- // Type string `json:"type"`
- BoundaryGap *bool `json:"boundaryGap"`
- SplitNumber int `json:"splitNumber"`
- Data []string `json:"data"`
- }
-}
-
-func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
- if len(data) == 0 {
- return nil
+ ds := make([]*_EChartsMarkData, 0)
+ err := json.Unmarshal(data, &ds)
+ if err != nil {
+ return err
}
- return json.Unmarshal(data, &ex.Data)
+ for _, d := range ds {
+ if d.Type != "" {
+ emd.Type = d.Type
+ }
+ }
+ return nil
}
-type ECharsOptions struct {
- Theme string `json:"theme"`
- Padding EChartsPadding `json:"padding"`
- Title struct {
- Text string `json:"text"`
- // 暂不支持(go-chart默认title只能居中)
- TextAlign string `json:"textAlign"`
- TextStyle struct {
- Color string `json:"color"`
- // TODO 字体支持
- FontFamily string `json:"fontFamily"`
- FontSize float64 `json:"fontSize"`
- Height float64 `json:"height"`
- } `json:"textStyle"`
- } `json:"title"`
- XAxis EChartsXAxis `json:"xAxis"`
- YAxis EChartsYAxis `json:"yAxis"`
- Legend struct {
- Data []string `json:"data"`
- Align string `json:"align"`
- Padding EChartsPadding `json:"padding"`
- Left LegendPostion `json:"left"`
- Right LegendPostion `json:"right"`
- // Top string `json:"top"`
- // Bottom string `json:"bottom"`
- } `json:"legend"`
- Series []struct {
- Data []ECharsSeriesData `json:"data"`
- Type string `json:"type"`
- YAxisIndex int `json:"yAxisIndex"`
- ItemStyle EChartStyle `json:"itemStyle"`
- } `json:"series"`
+type EChartsMarkPoint struct {
+ SymbolSize int `json:"symbolSize"`
+ Data []EChartsMarkData `json:"data"`
}
-func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) {
- tickPosition := chart.TickPositionUnset
-
- if len(e.Series) == 0 {
- return nil, tickPosition
+func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint {
+ sp := SeriesMarkPoint{
+ SymbolSize: emp.SymbolSize,
}
- seriesType := e.Series[0].Type
- if seriesType == SeriesPie {
- series := make([]Series, len(e.Series[0].Data))
- for index, item := range e.Series[0].Data {
- style := chart.Style{}
- if item.ItemStyle.Color != "" {
- c := parseColor(item.ItemStyle.Color)
- style.FillColor = c
- style.StrokeColor = c
- }
+ if len(emp.Data) == 0 {
+ return sp
+ }
+ data := make([]SeriesMarkData, len(emp.Data))
+ for index, item := range emp.Data {
+ data[index].Type = item.Type
+ }
+ sp.Data = data
+ return sp
+}
- series[index] = Series{
- Style: style,
- Data: []SeriesData{
- {
- Value: item.Value,
+type EChartsMarkLine struct {
+ Data []EChartsMarkData `json:"data"`
+}
+
+func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine {
+ sl := SeriesMarkLine{}
+ if len(eml.Data) == 0 {
+ return sl
+ }
+ data := make([]SeriesMarkData, len(eml.Data))
+ for index, item := range eml.Data {
+ data[index].Type = item.Type
+ }
+ sl.Data = data
+ return sl
+}
+
+type EChartsSeries struct {
+ Data []EChartsSeriesData `json:"data"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Radius string `json:"radius"`
+ YAxisIndex int `json:"yAxisIndex"`
+ ItemStyle EChartStyle `json:"itemStyle"`
+ // label的配置
+ Label EChartsLabelOption `json:"label"`
+ MarkPoint EChartsMarkPoint `json:"markPoint"`
+ MarkLine EChartsMarkLine `json:"markLine"`
+ Max *float64 `json:"max"`
+ Min *float64 `json:"min"`
+}
+type EChartsSeriesList []EChartsSeries
+
+func (esList EChartsSeriesList) ToSeriesList() SeriesList {
+ seriesList := make(SeriesList, 0, len(esList))
+ for _, item := range esList {
+ // 如果是pie,则每个子荐生成一个series
+ if item.Type == ChartTypePie {
+ for _, dataItem := range item.Data {
+ seriesList = append(seriesList, Series{
+ Type: item.Type,
+ Name: dataItem.Name,
+ Label: SeriesLabel{
+ Show: true,
},
- },
- Type: seriesType,
- Name: item.Name,
+ Radius: item.Radius,
+ Data: []SeriesData{
+ {
+ Value: dataItem.Value.First(),
+ },
+ },
+ })
}
+ continue
}
- return series, tickPosition
- }
- series := make([]Series, len(e.Series))
- for index, item := range e.Series {
- // bar默认tick居中
- if item.Type == SeriesBar {
- tickPosition = chart.TickPositionBetweenTicks
- }
- style := chart.Style{}
- if item.ItemStyle.Color != "" {
- c := parseColor(item.ItemStyle.Color)
- style.FillColor = c
- style.StrokeColor = c
+ // 如果是radar或funnel
+ if item.Type == ChartTypeRadar ||
+ item.Type == ChartTypeFunnel {
+ for _, dataItem := range item.Data {
+ seriesList = append(seriesList, Series{
+ Name: dataItem.Name,
+ Type: item.Type,
+ 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
}
data := make([]SeriesData, len(item.Data))
- for j, itemData := range item.Data {
- sd := SeriesData{
- Value: itemData.Value,
+ for j, dataItem := range item.Data {
+ data[j] = SeriesData{
+ Value: dataItem.Value.First(),
+ Style: dataItem.ItemStyle.ToStyle(),
}
- if itemData.ItemStyle.Color != "" {
- c := parseColor(itemData.ItemStyle.Color)
- sd.Style.FillColor = c
- sd.Style.StrokeColor = c
- }
- data[j] = sd
- }
- series[index] = Series{
- Style: style,
- YAxisIndex: item.YAxisIndex,
- Data: data,
- Type: item.Type,
}
+ seriesList = append(seriesList, Series{
+ Type: item.Type,
+ Data: data,
+ AxisIndex: item.YAxisIndex,
+ Style: item.ItemStyle.ToStyle(),
+ Label: SeriesLabel{
+ Color: parseColor(item.Label.Color),
+ Show: item.Label.Show,
+ Distance: item.Label.Distance,
+ },
+ Name: item.Name,
+ MarkPoint: item.MarkPoint.ToSeriesMarkPoint(),
+ MarkLine: item.MarkLine.ToSeriesMarkLine(),
+ })
}
- return series, tickPosition
+ return seriesList
}
-func (e *ECharsOptions) ToOptions() Options {
- o := Options{
- Theme: e.Theme,
- Padding: e.Padding.box,
- }
+type EChartsTextStyle struct {
+ Color string `json:"color"`
+ FontFamily string `json:"fontFamily"`
+ FontSize float64 `json:"fontSize"`
+}
- titleTextStyle := e.Title.TextStyle
- o.Title = Title{
- Text: e.Title.Text,
- Style: chart.Style{
- FontColor: parseColor(titleTextStyle.Color),
- FontSize: titleTextStyle.FontSize,
+func (et *EChartsTextStyle) ToStyle() chart.Style {
+ s := chart.Style{
+ FontSize: et.FontSize,
+ FontColor: parseColor(et.Color),
+ }
+ if et.FontFamily != "" {
+ s.Font, _ = GetFont(et.FontFamily)
+ }
+ return s
+}
+
+type EChartsOption struct {
+ Type string `json:"type"`
+ Theme string `json:"theme"`
+ FontFamily string `json:"fontFamily"`
+ Padding EChartsPadding `json:"padding"`
+ Box chart.Box `json:"box"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Title struct {
+ Text string `json:"text"`
+ Subtext string `json:"subtext"`
+ Left EChartsPosition `json:"left"`
+ Top EChartsPosition `json:"top"`
+ TextStyle EChartsTextStyle `json:"textStyle"`
+ SubtextStyle EChartsTextStyle `json:"subtextStyle"`
+ } `json:"title"`
+ XAxis EChartsXAxis `json:"xAxis"`
+ YAxis EChartsYAxis `json:"yAxis"`
+ Legend EChartsLegend `json:"legend"`
+ Radar struct {
+ Indicator []RadarIndicator `json:"indicator"`
+ } `json:"radar"`
+ Series EChartsSeriesList `json:"series"`
+ Children []EChartsOption `json:"children"`
+}
+
+func (eo *EChartsOption) ToOption() ChartOption {
+ fontFamily := eo.FontFamily
+ if len(fontFamily) == 0 {
+ fontFamily = eo.Title.TextStyle.FontFamily
+ }
+ titleTextStyle := eo.Title.TextStyle.ToStyle()
+ titleSubtextStyle := eo.Title.SubtextStyle.ToStyle()
+ legendTextStyle := eo.Legend.TextStyle.ToStyle()
+ o := ChartOption{
+ Type: eo.Type,
+ FontFamily: fontFamily,
+ Theme: eo.Theme,
+ Title: TitleOption{
+ Text: eo.Title.Text,
+ Subtext: eo.Title.Subtext,
+ FontColor: titleTextStyle.FontColor,
+ FontSize: titleTextStyle.FontSize,
+ SubtextFontSize: titleSubtextStyle.FontSize,
+ SubtextFontColor: titleSubtextStyle.FontColor,
+ Left: string(eo.Title.Left),
+ Top: string(eo.Title.Top),
},
+ Legend: LegendOption{
+ Show: eo.Legend.Show,
+ FontSize: legendTextStyle.FontSize,
+ FontColor: legendTextStyle.FontColor,
+ Data: eo.Legend.Data,
+ Left: string(eo.Legend.Left),
+ Top: string(eo.Legend.Top),
+ Align: eo.Legend.Align,
+ Orient: eo.Legend.Orient,
+ },
+ RadarIndicators: eo.Radar.Indicator,
+ Width: eo.Width,
+ Height: eo.Height,
+ Padding: eo.Padding.Box,
+ Box: eo.Box,
+ SeriesList: eo.Series.ToSeriesList(),
}
-
- if titleTextStyle.FontSize != 0 && titleTextStyle.Height > titleTextStyle.FontSize {
- padding := int(titleTextStyle.Height-titleTextStyle.FontSize) / 2
- o.Title.Style.Padding.Top = padding
- o.Title.Style.Padding.Bottom = padding
- }
-
- boundaryGap := false
- if len(e.XAxis.Data) != 0 {
- xAxis := e.XAxis.Data[0]
- o.XAxis = XAxis{
- Data: xAxis.Data,
- SplitNumber: xAxis.SplitNumber,
- }
- if xAxis.BoundaryGap == nil || *xAxis.BoundaryGap {
- boundaryGap = true
+ isHorizontalChart := false
+ for _, item := range eo.XAxis.Data {
+ if item.Type == "value" {
+ isHorizontalChart = true
}
}
-
- o.Legend = Legend{
- Data: e.Legend.Data,
- Align: e.Legend.Align,
- Padding: e.Legend.Padding.box,
- Left: string(e.Legend.Left),
- Right: string(e.Legend.Right),
- }
- if len(e.YAxis.Data) != 0 {
- yAxisOptions := make([]*YAxisOption, len(e.YAxis.Data))
- for index, item := range e.YAxis.Data {
- opt := &YAxisOption{
- Max: item.Max,
- Min: item.Min,
+ if isHorizontalChart {
+ for index := range o.SeriesList {
+ series := o.SeriesList[index]
+ if series.Type == ChartTypeBar {
+ o.SeriesList[index].Type = ChartTypeHorizontalBar
}
- template := item.AxisLabel.Formatter
- if template != "" {
- opt.Formater = func(v interface{}) string {
- str := defaultFloatFormater(v)
- return strings.ReplaceAll(template, "{value}", str)
- }
- }
- yAxisOptions[index] = opt
}
- o.YAxisOptions = yAxisOptions
}
- series, tickPosition := convertEChartsSeries(e)
-
- o.Series = series
-
- if boundaryGap {
- tickPosition = chart.TickPositionBetweenTicks
+ if len(eo.XAxis.Data) != 0 {
+ xAxisData := eo.XAxis.Data[0]
+ o.XAxis = XAxisOption{
+ BoundaryGap: xAxisData.BoundaryGap,
+ Data: xAxisData.Data,
+ SplitNumber: xAxisData.SplitNumber,
+ }
+ }
+ yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data))
+ for index, item := range eo.YAxis.Data {
+ yAxisOptions[index] = YAxisOption{
+ Min: item.Min,
+ Max: item.Max,
+ Formatter: item.AxisLabel.Formatter,
+ Color: parseColor(item.AxisLine.LineStyle.Color),
+ Data: item.Data,
+ }
+ }
+ o.YAxisOptions = yAxisOptions
+
+ if len(eo.Children) != 0 {
+ o.Children = make([]ChartOption, len(eo.Children))
+ for index, item := range eo.Children {
+ o.Children[index] = item.ToOption()
+ }
}
- o.TickPosition = tickPosition
return o
}
-func ParseECharsOptions(options string) (Options, error) {
- e := ECharsOptions{}
- err := json.Unmarshal([]byte(options), &e)
- if err != nil {
- return Options{}, err
- }
-
- return e.ToOptions(), nil
-}
-
-func echartsRender(options string, rp chart.RendererProvider) ([]byte, error) {
- o, err := ParseECharsOptions(options)
+func renderEcharts(options, outputType string) ([]byte, error) {
+ o := EChartsOption{}
+ err := json.Unmarshal([]byte(options), &o)
if err != nil {
return nil, err
}
- g, err := New(o)
+ opt := o.ToOption()
+ opt.Type = outputType
+ d, err := Render(opt)
if err != nil {
return nil, err
}
- return render(g, rp)
+ return d.Bytes()
}
func RenderEChartsToPNG(options string) ([]byte, error) {
- return echartsRender(options, chart.PNG)
+ return renderEcharts(options, "png")
}
func RenderEChartsToSVG(options string) ([]byte, error) {
- return echartsRender(options, chart.SVG)
+ return renderEcharts(options, "svg")
}
diff --git a/echarts_test.go b/echarts_test.go
index 9639ac1..2077278 100644
--- a/echarts_test.go
+++ b/echarts_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -27,395 +27,556 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/drawing"
+ "git.smarteching.com/zeni/go-chart/v2/drawing"
)
func TestConvertToArray(t *testing.T) {
assert := assert.New(t)
- assert.Nil(convertToArray([]byte(" ")))
-
- assert.Equal([]byte("[{}]"), convertToArray([]byte("{}")))
- assert.Equal([]byte("[{}]"), convertToArray([]byte("[{}]")))
+ assert.Equal([]byte(`[1]`), convertToArray([]byte("1")))
+ assert.Equal([]byte(`[1]`), convertToArray([]byte("[1]")))
}
-func TestECharsSeriesData(t *testing.T) {
+func TestEChartsPosition(t *testing.T) {
+ assert := assert.New(t)
+ var p EChartsPosition
+ err := p.UnmarshalJSON([]byte("1"))
+ assert.Nil(err)
+ assert.Equal(EChartsPosition("1"), p)
+ err = p.UnmarshalJSON([]byte(`"left"`))
+ assert.Nil(err)
+ assert.Equal(EChartsPosition("left"), p)
+}
+
+func TestEChartsSeriesDataValue(t *testing.T) {
assert := assert.New(t)
- es := ECharsSeriesData{}
- err := es.UnmarshalJSON([]byte(" "))
+ es := EChartsSeriesDataValue{}
+ err := es.UnmarshalJSON([]byte(`[1, 2]`))
assert.Nil(err)
- assert.Equal(ECharsSeriesData{}, es)
-
- es = ECharsSeriesData{}
- err = es.UnmarshalJSON([]byte("12.1"))
- assert.Nil(err)
- assert.Equal(ECharsSeriesData{
- Value: 12.1,
- }, es)
-
- es = ECharsSeriesData{}
- err = es.UnmarshalJSON([]byte(`{
- "value": 12.1,
- "name": "test",
- "itemStyle": {
- "color": "#333"
- }
- }`))
- assert.Nil(err)
- assert.Equal(ECharsSeriesData{
- Value: 12.1,
- Name: "test",
- ItemStyle: EChartStyle{
- Color: "#333",
+ assert.Equal(EChartsSeriesDataValue{
+ values: []float64{
+ 1,
+ 2,
},
}, es)
+ assert.Equal(NewEChartsSeriesDataValue(1, 2), es)
+ assert.Equal(1.0, es.First())
+}
+
+func TestEChartsSeriesData(t *testing.T) {
+ assert := assert.New(t)
+ es := EChartsSeriesData{}
+ err := es.UnmarshalJSON([]byte("1.1"))
+ assert.Nil(err)
+ assert.Equal(EChartsSeriesDataValue{
+ values: []float64{
+ 1.1,
+ },
+ }, es.Value)
+
+ err = es.UnmarshalJSON([]byte(`{"value":200,"itemStyle":{"color":"#a90000"}}`))
+ assert.Nil(err)
+ assert.Nil(err)
+ assert.Equal(EChartsSeriesData{
+ Value: EChartsSeriesDataValue{
+ values: []float64{
+ 200.0,
+ },
+ },
+ ItemStyle: EChartStyle{
+ Color: "#a90000",
+ },
+ }, es)
+}
+
+func TestEChartsXAxis(t *testing.T) {
+ assert := assert.New(t)
+ ex := EChartsXAxis{}
+ err := ex.UnmarshalJSON([]byte(`{"boundaryGap": true, "splitNumber": 5, "data": ["a", "b"], "type": "value"}`))
+ assert.Nil(err)
+
+ assert.Equal(EChartsXAxis{
+ Data: []EChartsXAxisData{
+ {
+ BoundaryGap: TrueFlag(),
+ SplitNumber: 5,
+ Data: []string{
+ "a",
+ "b",
+ },
+ Type: "value",
+ },
+ },
+ }, ex)
+}
+
+func TestEChartStyle(t *testing.T) {
+ assert := assert.New(t)
+
+ es := EChartStyle{
+ Color: "#999",
+ }
+ color := drawing.Color{
+ R: 153,
+ G: 153,
+ B: 153,
+ A: 255,
+ }
+ assert.Equal(Style{
+ FillColor: color,
+ FontColor: color,
+ StrokeColor: color,
+ }, es.ToStyle())
}
func TestEChartsPadding(t *testing.T) {
assert := assert.New(t)
- ep := EChartsPadding{}
- err := ep.UnmarshalJSON([]byte(" "))
- assert.Nil(err)
- assert.Equal(EChartsPadding{}, ep)
+ eb := EChartsPadding{}
- ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("1"))
+ err := eb.UnmarshalJSON([]byte(`1`))
assert.Nil(err)
- assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Left: 1,
- Right: 1,
- Bottom: 1,
- },
- }, ep)
+ assert.Equal(Box{
+ Left: 1,
+ Top: 1,
+ Right: 1,
+ Bottom: 1,
+ }, eb.Box)
- ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2]"))
+ err = eb.UnmarshalJSON([]byte(`[2, 3]`))
assert.Nil(err)
- assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Left: 2,
- Right: 2,
- Bottom: 1,
- },
- }, ep)
+ assert.Equal(Box{
+ Left: 3,
+ Top: 2,
+ Right: 3,
+ Bottom: 2,
+ }, eb.Box)
- ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2, 3]"))
+ err = eb.UnmarshalJSON([]byte(`[4, 5, 6]`))
assert.Nil(err)
- assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Right: 2,
- Bottom: 3,
- Left: 2,
- },
- }, ep)
+ assert.Equal(Box{
+ Left: 5,
+ Top: 4,
+ Right: 5,
+ Bottom: 6,
+ }, eb.Box)
- ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2, 3, 4]"))
+ err = eb.UnmarshalJSON([]byte(`[4, 5, 6, 7]`))
assert.Nil(err)
- assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Right: 2,
- Bottom: 3,
- Left: 4,
- },
- }, ep)
+ assert.Equal(Box{
+ Left: 7,
+ Top: 4,
+ Right: 5,
+ Bottom: 6,
+ }, eb.Box)
}
-func TestConvertEChartsSeries(t *testing.T) {
+func TestEChartsMarkPoint(t *testing.T) {
assert := assert.New(t)
- seriesList, tickPosition := convertEChartsSeries(&ECharsOptions{})
- assert.Empty(seriesList)
- assert.Equal(chart.TickPositionUnset, tickPosition)
-
- e := ECharsOptions{}
- err := json.Unmarshal([]byte(`{
- "title": {
- "text": "Referer of a Website"
- },
- "series": [
+ emp := EChartsMarkPoint{
+ SymbolSize: 30,
+ Data: []EChartsMarkData{
{
- "name": "Access From",
- "type": "pie",
- "radius": "50%",
- "data": [
+ Type: "test",
+ },
+ },
+ }
+ assert.Equal(SeriesMarkPoint{
+ SymbolSize: 30,
+ Data: []SeriesMarkData{
+ {
+ Type: "test",
+ },
+ },
+ }, emp.ToSeriesMarkPoint())
+}
+
+func TestEChartsMarkLine(t *testing.T) {
+ assert := assert.New(t)
+
+ eml := EChartsMarkLine{
+ Data: []EChartsMarkData{
+ {
+ Type: "min",
+ },
+ {
+ Type: "max",
+ },
+ },
+ }
+ assert.Equal(SeriesMarkLine{
+ Data: []SeriesMarkData{
+ {
+ Type: "min",
+ },
+ {
+ Type: "max",
+ },
+ },
+ }, eml.ToSeriesMarkLine())
+}
+
+func TestEChartsOption(t *testing.T) {
+ assert := assert.New(t)
+
+ tests := []struct {
+ option string
+ }{
+ {
+ option: `{
+ "xAxis": {
+ "type": "category",
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "type": "value"
+ },
+ "series": [
{
- "value": 1048,
- "name": "Search Engine"
- },
- {
- "value": 735,
- "name": "Direct"
+ "data": [
+ 120,
+ {
+ "value": 200,
+ "itemStyle": {
+ "color": "#a90000"
+ }
+ },
+ 150,
+ 80,
+ 70,
+ 110,
+ 130
+ ],
+ "type": "bar"
}
]
- }
- ]
- }`), &e)
- assert.Nil(err)
- seriesList, tickPosition = convertEChartsSeries(&e)
- assert.Equal(chart.TickPositionUnset, tickPosition)
- assert.Equal([]Series{
- {
- Data: []SeriesData{
- {
- Value: 1048,
- },
- },
- Type: SeriesPie,
- Name: "Search Engine",
+ }`,
},
{
- Data: []SeriesData{
- {
- Value: 735,
+ option: `{
+ "title": {
+ "text": "Referer of a Website",
+ "subtext": "Fake Data",
+ "left": "center"
},
- },
- Type: SeriesPie,
- Name: "Direct",
- },
- }, seriesList)
-
- err = json.Unmarshal([]byte(`{
- "series": [
- {
- "name": "Evaporation",
- "type": "bar",
- "data": [2, {
- "value": 4.9,
- "itemStyle": {
- "color": "#a90000"
+ "tooltip": {
+ "trigger": "item"
+ },
+ "legend": {
+ "orient": "vertical",
+ "left": "left"
+ },
+ "series": [
+ {
+ "name": "Access From",
+ "type": "pie",
+ "radius": "50%",
+ "data": [
+ {
+ "value": 1048,
+ "name": "Search Engine"
+ },
+ {
+ "value": 735,
+ "name": "Direct"
+ },
+ {
+ "value": 580,
+ "name": "Email"
+ },
+ {
+ "value": 484,
+ "name": "Union Ads"
+ },
+ {
+ "value": 300,
+ "name": "Video Ads"
+ }
+ ],
+ "emphasis": {
+ "itemStyle": {
+ "shadowBlur": 10,
+ "shadowOffsetX": 0,
+ "shadowColor": "rgba(0, 0, 0, 0.5)"
+ }
+ }
}
- }, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
- },
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
- }
- ]
- }`), &e)
- assert.Nil(err)
- bar1Data := NewSeriesDataListFromFloat([]float64{
- 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6,
- })
- bar1Data[1].Style.FillColor = parseColor("#a90000")
- bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor
-
- seriesList, tickPosition = convertEChartsSeries(&e)
- assert.Equal(chart.TickPositionBetweenTicks, tickPosition)
- assert.Equal([]Series{
- {
- Data: bar1Data,
- Type: SeriesBar,
+ ]
+ }`,
},
{
- Data: NewSeriesDataListFromFloat([]float64{
- 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6,
- }),
- Type: SeriesBar,
+ option: `{
+ "title": {
+ "text": "Rainfall vs Evaporation",
+ "subtext": "Fake Data"
+ },
+ "tooltip": {
+ "trigger": "axis"
+ },
+ "legend": {
+ "data": [
+ "Rainfall",
+ "Evaporation"
+ ]
+ },
+ "toolbox": {
+ "show": true,
+ "feature": {
+ "dataView": {
+ "show": true,
+ "readOnly": false
+ },
+ "magicType": {
+ "show": true,
+ "type": [
+ "line",
+ "bar"
+ ]
+ },
+ "restore": {
+ "show": true
+ },
+ "saveAsImage": {
+ "show": true
+ }
+ }
+ },
+ "calculable": true,
+ "xAxis": [
+ {
+ "type": "category",
+ "data": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ]
+ }
+ ],
+ "yAxis": [
+ {
+ "type": "value"
+ }
+ ],
+ "series": [
+ {
+ "name": "Rainfall",
+ "type": "bar",
+ "data": [
+ 2,
+ 4.9,
+ 7,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20,
+ 6.4,
+ 3.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max",
+ "name": "Max"
+ },
+ {
+ "type": "min",
+ "name": "Min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average",
+ "name": "Avg"
+ }
+ ]
+ }
+ },
+ {
+ "name": "Evaporation",
+ "type": "bar",
+ "data": [
+ 2.6,
+ 5.9,
+ 9,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6,
+ 2.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "name": "Max",
+ "value": 182.2,
+ "xAxis": 7,
+ "yAxis": 183
+ },
+ {
+ "name": "Min",
+ "value": 2.3,
+ "xAxis": 11,
+ "yAxis": 3
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average",
+ "name": "Avg"
+ }
+ ]
+ }
+ }
+ ]
+ }`,
},
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3,
- }),
- Type: SeriesLine,
- YAxisIndex: 1,
- },
- }, seriesList)
-
+ }
+ for _, tt := range tests {
+ opt := EChartsOption{}
+ err := json.Unmarshal([]byte(tt.option), &opt)
+ assert.Nil(err)
+ assert.NotEmpty(opt.Series)
+ assert.NotEmpty(opt.ToOption().SeriesList)
+ }
}
-func TestParseECharsOptions(t *testing.T) {
-
+func TestRenderEChartsToSVG(t *testing.T) {
assert := assert.New(t)
- options, err := ParseECharsOptions(`{
- "theme": "dark",
- "padding": [5, 10],
+
+ data, err := RenderEChartsToSVG(`{
"title": {
- "text": "Multi Line",
- "textAlign": "left",
- "textStyle": {
- "color": "#333",
- "fontSize": 24,
- "height": 40
- }
+ "text": "Rainfall vs Evaporation",
+ "subtext": "Fake Data"
},
"legend": {
- "align": "left",
- "padding": [5, 0, 0, 50],
- "data": ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"]
+ "data": [
+ "Rainfall",
+ "Evaporation"
+ ]
},
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
- "splitNumber": 10
- },
- "yAxis": [
+ "padding": [10, 30, 10, 10],
+ "xAxis": [
{
- "min": 0,
- "max": 250
- },
- {
- "min": 0,
- "max": 25
+ "type": "category",
+ "data": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ]
}
],
"series": [
+ {
+ "name": "Rainfall",
+ "type": "bar",
+ "data": [
+ 2,
+ 4.9,
+ 7,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20,
+ 6.4,
+ 3.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ }
+ ]
+ }
+ },
{
"name": "Evaporation",
"type": "bar",
- "data": [2, {
- "value": 4.9,
- "itemStyle": {
- "color": "#a90000"
- }
- }, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "itemStyle": {
- "color": "#0052d9"
+ "data": [
+ 2.6,
+ 5.9,
+ 9,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6,
+ 2.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
},
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
- },
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ }
+ ]
+ }
}
]
}`)
-
assert.Nil(err)
-
- min1 := float64(0)
- max1 := float64(250)
- min2 := float64(0)
- max2 := float64(25)
-
- bar1Data := NewSeriesDataListFromFloat([]float64{
- 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6,
- })
- bar1Data[1].Style.FillColor = parseColor("#a90000")
- bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor
-
- assert.Equal(Options{
- Theme: ThemeDark,
- Padding: chart.Box{
- Top: 5,
- Bottom: 5,
- Left: 10,
- Right: 10,
- },
- Title: Title{
- Text: "Multi Line",
- Style: chart.Style{
- FontColor: parseColor("#333"),
- FontSize: 24,
- Padding: chart.Box{
- Top: 8,
- Bottom: 8,
- },
- },
- },
- Legend: Legend{
- Data: []string{
- "Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
- },
- Align: "left",
- Padding: chart.Box{
- Top: 5,
- Right: 0,
- Bottom: 0,
- Left: 50,
- },
- },
- XAxis: XAxis{
- Data: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"},
- SplitNumber: 10,
- },
- TickPosition: chart.TickPositionBetweenTicks,
- YAxisOptions: []*YAxisOption{
- {
- Min: &min1,
- Max: &max1,
- },
- {
- Min: &min2,
- Max: &max2,
- },
- },
- Series: []Series{
- {
- Data: bar1Data,
- Type: SeriesBar,
- },
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6,
- }),
- Type: SeriesBar,
- Style: chart.Style{
- StrokeColor: drawing.Color{
- R: 0,
- G: 82,
- B: 217,
- A: 255,
- },
- FillColor: drawing.Color{
- R: 0,
- G: 82,
- B: 217,
- A: 255,
- },
- },
- },
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3,
- }),
- Type: SeriesLine,
- YAxisIndex: 1,
- },
- },
- }, options)
-}
-
-func BenchmarkEChartsRender(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, err := RenderEChartsToPNG(`{
- "title": {
- "text": "Line"
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260]
- }
- ]
- }`)
- if err != nil {
- panic(err)
- }
- }
+ assert.Equal("", string(data))
}
diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go
new file mode 100644
index 0000000..57ca1e9
--- /dev/null
+++ b/examples/area_line_chart/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+
+ "git.smarteching.com/zeni/go-charts/v2"
+)
+
+func writeFile(buf []byte) error {
+ tmpPath := "./tmp"
+ err := os.MkdirAll(tmpPath, 0700)
+ if err != nil {
+ return err
+ }
+
+ file := filepath.Join(tmpPath, "area-line-chart.png")
+ err = os.WriteFile(file, buf, 0600)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ values := [][]float64{
+ {
+ 120,
+ 132,
+ 101,
+ 134,
+ 90,
+ 230,
+ 210,
+ },
+ }
+ p, err := charts.LineRender(
+ values,
+ charts.TitleTextOptionFunc("Line"),
+ charts.XAxisDataOptionFunc([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }),
+ charts.LegendLabelsOptionFunc([]string{
+ "Email",
+ }, "50"),
+ func(opt *charts.ChartOption) {
+ opt.Legend.Padding = charts.Box{
+ Top: 5,
+ Bottom: 10,
+ }
+ opt.FillArea = true
+ },
+ )
+
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ err = writeFile(buf)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go
new file mode 100644
index 0000000..91c9f81
--- /dev/null
+++ b/examples/bar_chart/main.go
@@ -0,0 +1,102 @@
+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, "bar-chart.png")
+ err = os.WriteFile(file, buf, 0600)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ values := [][]float64{
+ {
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ },
+ {
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ },
+ }
+ p, err := charts.BarRender(
+ values,
+ charts.XAxisDataOptionFunc([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ charts.LegendLabelsOptionFunc([]string{
+ "Rainfall",
+ "Evaporation",
+ }, charts.PositionRight),
+ charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage),
+ charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin),
+ // custom option func
+ func(opt *charts.ChartOption) {
+ opt.SeriesList[1].MarkPoint = charts.NewMarkPoint(
+ charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin,
+ )
+ opt.SeriesList[1].MarkLine = charts.NewMarkLine(
+ charts.SeriesMarkDataTypeAverage,
+ )
+ },
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ buf, err := p.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ err = writeFile(buf)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/examples/basic/main.go b/examples/basic/main.go
deleted file mode 100644
index c019df3..0000000
--- a/examples/basic/main.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
- "os"
-
- charts "github.com/vicanso/go-echarts"
-)
-
-func main() {
- buf, err := charts.RenderEChartsToPNG(`{
- "title": {
- "text": "Line"
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260]
- }
- ]
- }`)
- if err != nil {
- panic(err)
- }
- file, err := os.Create("output.png")
- if err != nil {
- panic(err)
- }
- defer file.Close()
- file.Write(buf)
-}
diff --git a/examples/charts/main.go b/examples/charts/main.go
index 4316d79..81bc4f2 100644
--- a/examples/charts/main.go
+++ b/examples/charts/main.go
@@ -2,9 +2,11 @@ package main
import (
"bytes"
+ "fmt"
"net/http"
+ "strconv"
- charts "github.com/vicanso/go-echarts"
+ charts "git.smarteching.com/zeni/go-charts/v2"
)
var html = `
@@ -12,11 +14,16 @@ var html = `
-
- go-echarts
+ go-charts