diff --git a/README.md b/README.md index 55e5760..270a338 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# go-chart \ No newline at end of file +# go-echarts + +![Alt](https://repobeats.axiom.co/api/embed/9071915842d72a909465be75eb6c12ffb7de2dcf.svg "Repobeats analytics image") + +[go-chart](https://github.com/wcharczuk/go-chart)是golang常用的可视化图表库,支持`svg`与`png`的输出,`Apache ECharts`在前端开发中得到众多开发者的认可。go-echarts则是结合两者的方式,兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的几种图表截图: + +![](./assets/go-echarts.jpg) + +## 支持图表类型 + +暂仅支持三种的图表类型:`line`, `bar` 以及 `pie` + + +## 示例 + +`go-echarts`兼容了`echarts`的参数配置,可简单的使用json形式的配置字符串则可快速生成图表。 + +```go +package main + +import ( + "os" + + charts "github.com/vicanso/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) + } + os.WriteFile("output.png", buf, 0600) +} +``` \ No newline at end of file diff --git a/assets/go-echarts.jpg b/assets/go-echarts.jpg new file mode 100644 index 0000000..85032e5 Binary files /dev/null and b/assets/go-echarts.jpg differ diff --git a/axis.go b/axis.go index 5b2be9e..42569a3 100644 --- a/axis.go +++ b/axis.go @@ -50,6 +50,16 @@ type YAxisOption struct { const axisStrokeWidth = 1 +func maxInt(values ...int) int { + result := 0 + for _, v := range values { + if v > result { + result = v + } + } + return result +} + // 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 @@ -66,10 +76,8 @@ func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme strin xValues := make([]float64, size) ticks := make([]chart.Tick, 0) - maxTicks := xAxis.SplitNumber - if maxTicks == 0 { - maxTicks = 10 - } + // tick width + maxTicks := maxInt(xAxis.SplitNumber, 10) // 计息最多每个unit至少放多个 minUnitSize := originalSize / maxTicks diff --git a/echarts.go b/echarts.go index 02cede2..672a21a 100644 --- a/echarts.go +++ b/echarts.go @@ -188,6 +188,7 @@ type ECharsOptions struct { Data []ECharsSeriesData `json:"data"` Type string `json:"type"` YAxisIndex int `json:"yAxisIndex"` + ItemStyle EChartStyle `json:"itemStyle"` } `json:"series"` } @@ -201,7 +202,15 @@ func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) { 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 + } + series[index] = Series{ + Style: style, Data: []SeriesData{ { Value: item.Value, @@ -219,6 +228,12 @@ func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) { 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 + } data := make([]SeriesData, len(item.Data)) for j, itemData := range item.Data { sd := SeriesData{ @@ -232,6 +247,7 @@ func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) { data[j] = sd } series[index] = Series{ + Style: style, YAxisIndex: item.YAxisIndex, Data: data, Type: item.Type, @@ -317,3 +333,23 @@ func ParseECharsOptions(options string) (Options, error) { return e.ToOptions(), nil } + +func echartsRender(options string, rp chart.RendererProvider) ([]byte, error) { + o, err := ParseECharsOptions(options) + if err != nil { + return nil, err + } + g, err := New(o) + if err != nil { + return nil, err + } + return render(g, rp) +} + +func RenderEChartsToPNG(options string) ([]byte, error) { + return echartsRender(options, chart.PNG) +} + +func RenderEChartsToSVG(options string) ([]byte, error) { + return echartsRender(options, chart.SVG) +} diff --git a/echarts_test.go b/echarts_test.go index 99bf414..b06da53 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestConvertToArray(t *testing.T) { @@ -288,6 +289,9 @@ func TestParseECharsOptions(t *testing.T) { { "name": "Precipitation", "type": "bar", + "itemStyle": { + "color": "#0052d9" + }, "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6] }, { @@ -368,6 +372,20 @@ func TestParseECharsOptions(t *testing.T) { 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{ diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..906f32c --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + + charts "github.com/vicanso/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) + } + os.WriteFile("output.png", buf, 0600) +} diff --git a/examples/main.go b/examples/charts/main.go similarity index 79% rename from examples/main.go rename to examples/charts/main.go index c07b49d..4cf72e2 100644 --- a/examples/main.go +++ b/examples/charts/main.go @@ -9,32 +9,44 @@ import ( var html = ` - - - - - - - go-echarts - - - {{body}} - + + + + + + + go-echarts + + +
{{body}}
+ ` @@ -43,10 +55,9 @@ var chartOptions = []map[string]string{ "title": "折线图", "option": `{ "title": { - "text": "line", + "text": "Line", "textAlign": "left", "textStyle": { - "color": "#333", "fontSize": 24, "height": 40 } @@ -214,6 +225,9 @@ var chartOptions = []map[string]string{ { "name": "Evaporation", "type": "bar", + "itemStyle": { + "color": "#0052d9" + }, "data": [2, 4.9, 7, 23.2, 25.6, 76.7, 135.6] }, { @@ -241,7 +255,7 @@ var chartOptions = []map[string]string{ }, "xAxis": { "type": "category", - "splitNumber": 12, + "splitNumber": 6, "data": ["01-01","01-02","01-03","01-04","01-05","01-06","01-07","01-08","01-09","01-10","01-11","01-12","01-13","01-14","01-15","01-16","01-17","01-18","01-19","01-20","01-21","01-22","01-23","01-24","01-25","01-26","01-27","01-28","01-29","01-30","01-31"] }, "yAxis": { @@ -300,19 +314,35 @@ var chartOptions = []map[string]string{ }, } -func render(theme string) ([]byte, error) { +type renderOptions struct { + theme string + width int + height int + onlyCharts bool +} + +func render(opts renderOptions) ([]byte, error) { data := bytes.Buffer{} for _, m := range chartOptions { - // if m["title"] != "多柱状图" { - // continue - // } chartHTML := []byte(`

{{title}}

{{option}}
{{svg}}
`) + if opts.onlyCharts { + chartHTML = []byte(`
+ {{svg}} +
`) + } o, err := charts.ParseECharsOptions(m["option"]) - o.Theme = theme + if opts.width > 0 { + o.Width = opts.width + } + if opts.height > 0 { + o.Height = opts.height + } + + o.Theme = opts.theme if err != nil { return nil, err } @@ -333,8 +363,17 @@ func render(theme string) ([]byte, error) { } func indexHandler(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + opts := renderOptions{ + theme: query.Get("theme"), + } + if query.Get("view") == "grid" { + opts.width = 400 + opts.height = 200 + opts.onlyCharts = true + } - buf, err := render(r.URL.Query().Get("theme")) + buf, err := render(opts) if err != nil { w.WriteHeader(400) w.Write([]byte(err.Error())) diff --git a/go.mod b/go.mod index 0f43724..6ea9001 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index c0579a4..51e8735 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= @@ -11,10 +12,14 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/series.go b/series.go index 37e6c71..36e57bd 100644 --- a/series.go +++ b/series.go @@ -37,6 +37,7 @@ type Series struct { Data []SeriesData XValues []float64 YAxisIndex int + Style chart.Style } const lineStrokeWidth = 2 @@ -75,6 +76,10 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [ DotColor: getSeriesColor(theme, index), DotWidth: dotWith, } + if !item.Style.StrokeColor.IsZero() { + style.StrokeColor = item.Style.StrokeColor + style.DotColor = item.Style.StrokeColor + } pointIndexOffset := 0 // 如果居中,需要多增加一个点 if tickPosition == chart.TickPositionBetweenTicks {