feat: support echart options

This commit is contained in:
vicanso 2022-02-16 22:51:02 +08:00
parent b934b853a9
commit 519c8a492e
8 changed files with 803 additions and 2 deletions

View file

@ -125,6 +125,10 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
if !series.Label.Show {
continue
}
distance := series.Label.Distance
if distance == 0 {
distance = 5
}
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
labelStyle := chart.Style{
FontColor: theme.GetTextColor(),
@ -136,7 +140,7 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
}
labelStyle.GetTextOptions().WriteToRenderer(r)
textBox := r.MeasureText(text)
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance)
}
// 生成mark point的参数

View file

@ -25,6 +25,7 @@ package charts
import (
"errors"
"math"
"strings"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
@ -155,6 +156,10 @@ func (o *ChartOption) FillDefault(theme string) {
}
}
}
// 如果无legend数据则隐藏
if len(strings.Join(o.Legend.Data, "")) == 0 {
o.Legend.Show = FalseFlag()
}
if o.Legend.Style.Font == nil {
o.Legend.Style.Font = o.Font
}

407
echarts.go Normal file
View file

@ -0,0 +1,407 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strconv"
"github.com/wcharczuk/go-chart/v2"
)
func convertToArray(data []byte) []byte {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
if data[0] != '[' {
data = []byte("[" + string(data) + "]")
}
return 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() chart.Style {
color := parseColor(es.Color)
return chart.Style{
FillColor: color,
FontColor: color,
StrokeColor: color,
}
}
type EChartsSeriesData struct {
Value float64 `json:"value"`
Name string `json:"name"`
ItemStyle EChartStyle `json:"itemStyle"`
}
type _EChartsSeriesData EChartsSeriesData
func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
if regexp.MustCompile(`^\d+`).Match(data) {
v, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
}
es.Value = v
return nil
}
v := _EChartsSeriesData{}
err := json.Unmarshal(data, &v)
if err != nil {
return err
}
es.Name = v.Name
es.Value = v.Value
es.ItemStyle = v.ItemStyle
return nil
}
type EChartsXAxisData struct {
BoundaryGap *bool `json:"boundaryGap"`
SplitNumber int `json:"splitNumber"`
Data []string `json:"data"`
}
type EChartsXAxis struct {
Data []EChartsXAxisData
}
func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
if len(data) == 0 {
return nil
}
return json.Unmarshal(data, &ex.Data)
}
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
}
}
}
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
}
arr := make([]int, 0)
err := json.Unmarshal(data, &arr)
if err != nil {
return err
}
if len(arr) == 0 {
return nil
}
switch len(arr) {
case 1:
eb.Box = chart.Box{
Left: arr[0],
Top: arr[0],
Bottom: arr[0],
Right: arr[0],
}
case 2:
eb.Box = chart.Box{
Top: arr[0],
Bottom: arr[0],
Left: arr[1],
Right: arr[1],
}
default:
result := make([]int, 4)
copy(result, arr)
if len(arr) == 3 {
result[3] = result[1]
}
// 上右下左
eb.Box = chart.Box{
Top: result[0],
Right: result[1],
Bottom: result[2],
Left: result[3],
}
}
return nil
}
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"`
}
type EChartsMarkPoint struct {
SymbolSize int `json:"symbolSize"`
Data []struct {
Type string `json:"type"`
} `json:"data"`
}
func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint {
data := make([]SeriesMarkData, len(emp.Data))
for index, item := range emp.Data {
data[index] = SeriesMarkData{
Type: item.Type,
}
}
return SeriesMarkPoint{
Data: data,
SymbolSize: emp.SymbolSize,
}
}
type EChartsMarkLine struct {
Data []struct {
Type string `json:"type"`
} `json:"data"`
}
func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine {
data := make([]SeriesMarkData, len(eml.Data))
for index, item := range eml.Data {
data[index] = SeriesMarkData{
Type: item.Type,
}
}
return SeriesMarkLine{
Data: data,
}
}
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"`
}
type EChartsSeriesList []EChartsSeries
func (esList EChartsSeriesList) ToSeriesList() SeriesList {
seriesList := make(SeriesList, len(esList))
for index, item := range esList {
data := make([]SeriesData, len(item.Data))
for j, dataItem := range item.Data {
data[j] = SeriesData{
Value: dataItem.Value,
Style: dataItem.ItemStyle.ToStyle(),
}
}
seriesList[index] = Series{
Type: item.Type,
Data: data,
YAxisIndex: item.YAxisIndex,
Style: item.ItemStyle.ToStyle(),
Label: SeriesLabel{
Color: parseColor(item.Label.Color),
Show: item.Label.Show,
Distance: item.Label.Distance,
},
Name: item.Name,
Radius: item.Radius,
MarkPoint: item.MarkPoint.ToSeriesMarkPoint(),
MarkLine: item.MarkLine.ToSeriesMarkLine(),
}
}
return seriesList
}
type EChartsTextStyle struct {
Color string `json:"color"`
FontFamily string `json:"fontFamily"`
FontSize float64 `json:"fontSize"`
}
func (et *EChartsTextStyle) ToStyle() chart.Style {
return chart.Style{
FontSize: et.FontSize,
FontColor: parseColor(et.Color),
}
}
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"`
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
}
o := ChartOption{
Type: eo.Type,
FontFamily: fontFamily,
Theme: eo.Theme,
Title: TitleOption{
Text: eo.Title.Text,
Subtext: eo.Title.Subtext,
Style: eo.Title.TextStyle.ToStyle(),
SubtextStyle: eo.Title.SubtextStyle.ToStyle(),
Left: string(eo.Title.Left),
Top: string(eo.Title.Top),
},
Legend: LegendOption{
Show: eo.Legend.Show,
Style: eo.Legend.TextStyle.ToStyle(),
Data: eo.Legend.Data,
Left: string(eo.Legend.Left),
Top: string(eo.Legend.Top),
Align: eo.Legend.Align,
Orient: eo.Legend.Orient,
},
Width: eo.Width,
Height: eo.Height,
Padding: eo.Padding.Box,
Box: eo.Box,
SeriesList: eo.Series.ToSeriesList(),
}
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),
}
}
if len(eo.Children) != 0 {
o.Children = make([]ChartOption, len(eo.Children))
for index, item := range eo.Children {
o.Children[index] = item.ToOption()
}
}
return o
}
func renderEcharts(options, outputType string) ([]byte, error) {
o := EChartsOption{}
err := json.Unmarshal([]byte(options), &o)
if err != nil {
return nil, err
}
opt := o.ToOption()
opt.Type = outputType
d, err := Render(opt)
if err != nil {
return nil, err
}
return d.Bytes()
}
func RenderEChartsToPNG(options string) ([]byte, error) {
return renderEcharts(options, "png")
}
func RenderEChartsToSVG(options string) ([]byte, error) {
return renderEcharts(options, "svg")
}

301
echarts_test.go Normal file
View file

@ -0,0 +1,301 @@
// 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 (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
)
func TestEChartsPosition(t *testing.T) {
assert := assert.New(t)
var p EChartsPosition
err := p.UnmarshalJSON([]byte("12"))
assert.Nil(err)
assert.Equal("12", string(p))
err = p.UnmarshalJSON([]byte(`"12%"`))
assert.Nil(err)
assert.Equal("12%", string(p))
}
func TestEChartsXAxis(t *testing.T) {
assert := assert.New(t)
ex := EChartsXAxis{}
err := ex.UnmarshalJSON([]byte(`{
"boundaryGap": false,
"splitNumber": 5,
"data": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
]
}`))
assert.Nil(err)
assert.Equal(EChartsXAxis{
Data: []EChartsXAxisData{
{
BoundaryGap: FalseFlag(),
SplitNumber: 5,
Data: []string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
},
},
},
}, ex)
}
func TestEChartsYAxis(t *testing.T) {
assert := assert.New(t)
ey := EChartsYAxis{}
err := ey.UnmarshalJSON([]byte(`{
"min": 1,
"max": 10,
"axisLabel": {
"formatter": "ab"
}
}`))
assert.Nil(err)
assert.Equal(EChartsYAxis{
Data: []EChartsYAxisData{
{
Min: NewFloatPoint(1),
Max: NewFloatPoint(10),
AxisLabel: EChartsAxisLabel{
Formatter: "ab",
},
},
},
}, ey)
ey = EChartsYAxis{}
err = ey.UnmarshalJSON([]byte(`[
{
"min": 1,
"max": 10,
"axisLabel": {
"formatter": "ab"
}
},
{
"min": 2,
"max": 20,
"axisLabel": {
"formatter": "cd"
}
}
]`))
assert.Nil(err)
assert.Equal(EChartsYAxis{
Data: []EChartsYAxisData{
{
Min: NewFloatPoint(1),
Max: NewFloatPoint(10),
AxisLabel: EChartsAxisLabel{
Formatter: "ab",
},
},
{
Min: NewFloatPoint(2),
Max: NewFloatPoint(20),
AxisLabel: EChartsAxisLabel{
Formatter: "cd",
},
},
},
}, ey)
}
func TestEChartsPadding(t *testing.T) {
assert := assert.New(t)
ep := EChartsPadding{}
ep.UnmarshalJSON([]byte(`10`))
assert.Equal(EChartsPadding{
Box: chart.Box{
Top: 10,
Right: 10,
Bottom: 10,
Left: 10,
},
}, ep)
ep = EChartsPadding{}
ep.UnmarshalJSON([]byte(`[10, 20]`))
assert.Equal(EChartsPadding{
Box: chart.Box{
Top: 10,
Right: 20,
Bottom: 10,
Left: 20,
},
}, ep)
ep = EChartsPadding{}
ep.UnmarshalJSON([]byte(`[10, 20, 30]`))
assert.Equal(EChartsPadding{
Box: chart.Box{
Top: 10,
Right: 20,
Bottom: 30,
Left: 20,
},
}, ep)
ep = EChartsPadding{}
ep.UnmarshalJSON([]byte(`[10, 20, 30, 40]`))
assert.Equal(EChartsPadding{
Box: chart.Box{
Top: 10,
Right: 20,
Bottom: 30,
Left: 40,
},
}, ep)
}
func TestEChartsLegend(t *testing.T) {
assert := assert.New(t)
el := EChartsLegend{}
err := json.Unmarshal([]byte(`{
"data": ["a", "b", "c"],
"align": "right",
"padding": [10],
"left": "20%",
"top": 10
}`), &el)
assert.Nil(err)
assert.Equal(EChartsLegend{
Data: []string{
"a",
"b",
"c",
},
Align: "right",
Padding: EChartsPadding{
Box: chart.Box{
Left: 10,
Top: 10,
Right: 10,
Bottom: 10,
},
},
Left: EChartsPosition("20%"),
Top: EChartsPosition("10"),
}, el)
}
func TestEChartsSeriesData(t *testing.T) {
assert := assert.New(t)
esd := EChartsSeriesData{}
err := esd.UnmarshalJSON([]byte(`123`))
assert.Nil(err)
assert.Equal(EChartsSeriesData{
Value: 123,
}, esd)
esd = EChartsSeriesData{}
err = esd.UnmarshalJSON([]byte(`{
"value": 123.12,
"name": "test",
"itemStyle": {
"color": "#aaa"
}
}`))
assert.Nil(err)
assert.Equal(EChartsSeriesData{
Value: 123.12,
Name: "test",
ItemStyle: EChartStyle{
Color: "#aaa",
},
}, esd)
}
func TestEChartsSeries(t *testing.T) {
assert := assert.New(t)
esList := make([]EChartsSeries, 0)
err := json.Unmarshal([]byte(`[
{
"name": "Email",
"data": [
120,
132
]
},
{
"name": "Union Ads",
"type": "bar",
"data": [
220,
182
]
}
]`), &esList)
assert.Nil(err)
assert.Equal([]EChartsSeries{
{
Name: "Email",
Data: []EChartsSeriesData{
{
Value: 120,
},
{
Value: 132,
},
},
},
{
Name: "Union Ads",
Type: "bar",
Data: []EChartsSeriesData{
{
Value: 220,
},
{
Value: 182,
},
},
},
}, esList)
}

View file

@ -85,6 +85,10 @@ func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPoin
if !series.Label.Show {
continue
}
distance := series.Label.Distance
if distance == 0 {
distance = 5
}
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
labelStyle := chart.Style{
FontColor: theme.GetTextColor(),
@ -96,7 +100,7 @@ func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPoin
}
labelStyle.GetTextOptions().WriteToRenderer(r)
textBox := r.MeasureText(text)
d.text(text, x-textBox.Width()>>1, y-5)
d.text(text, x-textBox.Width()>>1, y-distance)
}
dotFillColor := drawing.ColorWhite

View file

@ -68,6 +68,8 @@ type SeriesLabel struct {
Color drawing.Color
// Show flag for label
Show bool
// Distance to the host graphic element.
Distance int
}
const (

38
util.go
View file

@ -23,11 +23,13 @@
package charts
import (
"regexp"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
func TrueFlag() *bool {
@ -130,3 +132,39 @@ func commafWithDigits(value float64) string {
}
return humanize.CommafWithDigits(value, decimals)
}
func parseColor(color string) drawing.Color {
c := drawing.Color{}
if color == "" {
return c
}
if strings.HasPrefix(color, "#") {
return drawing.ColorFromHex(color[1:])
}
reg := regexp.MustCompile(`\((\S+)\)`)
result := reg.FindAllStringSubmatch(color, 1)
if len(result) == 0 || len(result[0]) != 2 {
return c
}
arr := strings.Split(result[0][1], ",")
if len(arr) < 3 {
return c
}
// 设置默认为255
c.A = 255
for index, v := range arr {
value, _ := strconv.Atoi(strings.TrimSpace(v))
ui8 := uint8(value)
switch index {
case 0:
c.R = ui8
case 1:
c.G = ui8
case 2:
c.B = ui8
default:
c.A = ui8
}
}
return c
}

View file

@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
func TestGetDefaultInt(t *testing.T) {
@ -139,3 +140,42 @@ func TestConvertPercent(t *testing.T) {
assert.Equal(-1.0, convertPercent("a%"))
assert.Equal(0.1, convertPercent("10%"))
}
func TestParseColor(t *testing.T) {
assert := assert.New(t)
c := parseColor("")
assert.True(c.IsZero())
c = parseColor("#333")
assert.Equal(drawing.Color{
R: 51,
G: 51,
B: 51,
A: 255,
}, c)
c = parseColor("#313233")
assert.Equal(drawing.Color{
R: 49,
G: 50,
B: 51,
A: 255,
}, c)
c = parseColor("rgb(31,32,33)")
assert.Equal(drawing.Color{
R: 31,
G: 32,
B: 33,
A: 255,
}, c)
c = parseColor("rgba(50,51,52,250)")
assert.Equal(drawing.Color{
R: 50,
G: 51,
B: 52,
A: 250,
}, c)
}