feat: support radar option of echarts

This commit is contained in:
vicanso 2022-03-03 23:01:42 +08:00
parent 570828d35f
commit b93d096633
10 changed files with 411 additions and 56 deletions

View file

@ -152,6 +152,11 @@ The name with `[]` is new parameter, others are the same as `echarts`.
- `legend.padding` legend space around content - `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.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 - `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` The series for chart
- `series.name` Series name used for displaying in legend. - `series.name` Series name used for displaying in legend.
- `series.type` Series type: `line`, `bar` or`pie` - `series.type` Series type: `line`, `bar` or`pie`

File diff suppressed because one or more lines are too long

19
draw.go
View file

@ -322,3 +322,22 @@ func (d *Draw) polygon(center Point, radius float64, sides int) {
d.lineTo(points[0].X, points[0].Y) d.lineTo(points[0].X, points[0].Y)
d.Render.Stroke() d.Render.Stroke()
} }
func (d *Draw) fill(points []Point, s chart.Style) {
if !s.ShouldDrawFill() {
return
}
r := d.Render
var x, y int
s.GetFillOptions().WriteDrawingOptionsToRenderer(r)
for index, point := range points {
x = point.X
y = point.Y
if index == 0 {
d.moveTo(x, y)
} else {
d.lineTo(x, y)
}
}
r.Fill()
}

View file

@ -409,6 +409,56 @@ func TestDraw(t *testing.T) {
}, },
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"5\" cy=\"30\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 10 30\nL 289 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 289 25\nL 305 30\nL 289 35\nL 294 30\nL 289 25\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>", result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"5\" cy=\"30\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 10 30\nL 289 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 289 25\nL 305 30\nL 289 35\nL 294 30\nL 289 25\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
}, },
// polygon
{
fn: func(d *Draw) {
chart.Style{
StrokeWidth: 1,
StrokeColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
}.WriteToRenderer(d.Render)
d.polygon(Point{
X: 100,
Y: 100,
}, 50, 6)
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 105 60\nL 148 85\nL 148 134\nL 105 160\nL 62 135\nL 62 86\nL 105 60\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:none\"/></svg>",
},
// fill
{
fn: func(d *Draw) {
d.fill([]Point{
{
X: 0,
Y: 0,
},
{
X: 0,
Y: 100,
},
{
X: 100,
Y: 100,
},
{
X: 0,
Y: 0,
},
}, chart.Style{
FillColor: drawing.Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
})
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 10\nL 5 110\nL 105 110\nL 5 10\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/></svg>",
},
} }
for _, tt := range tests { for _, tt := range tests {
d, err := NewDraw(DrawOption{ d, err := NewDraw(DrawOption{

View file

@ -69,8 +69,28 @@ func (es *EChartStyle) ToStyle() chart.Style {
} }
} }
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 { type EChartsSeriesData struct {
Value float64 `json:"value"` Value EChartsSeriesDataValue `json:"value"`
Name string `json:"name"` Name string `json:"name"`
ItemStyle EChartStyle `json:"itemStyle"` ItemStyle EChartStyle `json:"itemStyle"`
} }
@ -88,7 +108,11 @@ func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
es.Value = v es.Value = EChartsSeriesDataValue{
values: []float64{
v,
},
}
return nil return nil
} }
v := _EChartsSeriesData{} v := _EChartsSeriesData{}
@ -291,7 +315,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
if item.Type == ChartTypePie { if item.Type == ChartTypePie {
for _, dataItem := range item.Data { for _, dataItem := range item.Data {
seriesList = append(seriesList, Series{ seriesList = append(seriesList, Series{
Type: ChartTypePie, Type: item.Type,
Name: dataItem.Name, Name: dataItem.Name,
Label: SeriesLabel{ Label: SeriesLabel{
Show: true, Show: true,
@ -299,17 +323,28 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
Radius: item.Radius, Radius: item.Radius,
Data: []SeriesData{ Data: []SeriesData{
{ {
Value: dataItem.Value, Value: dataItem.Value.First(),
}, },
}, },
}) })
} }
continue continue
} }
// 如果是radar
if item.Type == ChartTypeRadar {
for _, dataItem := range item.Data {
seriesList = append(seriesList, Series{
Name: dataItem.Name,
Type: item.Type,
Data: NewSeriesDataFromValues(dataItem.Value.values),
})
}
continue
}
data := make([]SeriesData, len(item.Data)) data := make([]SeriesData, len(item.Data))
for j, dataItem := range item.Data { for j, dataItem := range item.Data {
data[j] = SeriesData{ data[j] = SeriesData{
Value: dataItem.Value, Value: dataItem.Value.First(),
Style: dataItem.ItemStyle.ToStyle(), Style: dataItem.ItemStyle.ToStyle(),
} }
} }
@ -367,6 +402,9 @@ type EChartsOption struct {
XAxis EChartsXAxis `json:"xAxis"` XAxis EChartsXAxis `json:"xAxis"`
YAxis EChartsYAxis `json:"yAxis"` YAxis EChartsYAxis `json:"yAxis"`
Legend EChartsLegend `json:"legend"` Legend EChartsLegend `json:"legend"`
Radar struct {
Indicator []RadarIndicator `json:"indicator"`
} `json:"radar"`
Series EChartsSeriesList `json:"series"` Series EChartsSeriesList `json:"series"`
Children []EChartsOption `json:"children"` Children []EChartsOption `json:"children"`
} }
@ -397,6 +435,7 @@ func (eo *EChartsOption) ToOption() ChartOption {
Align: eo.Legend.Align, Align: eo.Legend.Align,
Orient: eo.Legend.Orient, Orient: eo.Legend.Orient,
}, },
RadarIndicators: eo.Radar.Indicator,
Width: eo.Width, Width: eo.Width,
Height: eo.Height, Height: eo.Height,
Padding: eo.Padding.Box, Padding: eo.Padding.Box,

View file

@ -253,14 +253,14 @@ func TestEChartsSeriesData(t *testing.T) {
err := esd.UnmarshalJSON([]byte(`123`)) err := esd.UnmarshalJSON([]byte(`123`))
assert.Nil(err) assert.Nil(err)
assert.Equal(EChartsSeriesData{ assert.Equal(EChartsSeriesData{
Value: 123, Value: NewEChartsSeriesDataValue(123),
}, esd) }, esd)
esd = EChartsSeriesData{} esd = EChartsSeriesData{}
err = esd.UnmarshalJSON([]byte(`2.1`)) err = esd.UnmarshalJSON([]byte(`2.1`))
assert.Nil(err) assert.Nil(err)
assert.Equal(EChartsSeriesData{ assert.Equal(EChartsSeriesData{
Value: 2.1, Value: NewEChartsSeriesDataValue(2.1),
}, esd) }, esd)
esd = EChartsSeriesData{} esd = EChartsSeriesData{}
@ -273,7 +273,7 @@ func TestEChartsSeriesData(t *testing.T) {
}`)) }`))
assert.Nil(err) assert.Nil(err)
assert.Equal(EChartsSeriesData{ assert.Equal(EChartsSeriesData{
Value: 123.12, Value: NewEChartsSeriesDataValue(123.12),
Name: "test", Name: "test",
ItemStyle: EChartStyle{ ItemStyle: EChartStyle{
Color: "#aaa", Color: "#aaa",
@ -308,10 +308,10 @@ func TestEChartsSeries(t *testing.T) {
Name: "Email", Name: "Email",
Data: []EChartsSeriesData{ Data: []EChartsSeriesData{
{ {
Value: 120, Value: NewEChartsSeriesDataValue(120),
}, },
{ {
Value: 132, Value: NewEChartsSeriesDataValue(132),
}, },
}, },
}, },
@ -320,10 +320,10 @@ func TestEChartsSeries(t *testing.T) {
Type: "bar", Type: "bar",
Data: []EChartsSeriesData{ Data: []EChartsSeriesData{
{ {
Value: 220, Value: NewEChartsSeriesDataValue(220),
}, },
{ {
Value: 182, Value: NewEChartsSeriesDataValue(182),
}, },
}, },
}, },
@ -431,11 +431,19 @@ func TestEChartsSeriesList(t *testing.T) {
Data: []EChartsSeriesData{ Data: []EChartsSeriesData{
{ {
Name: "1", Name: "1",
Value: 1, Value: EChartsSeriesDataValue{
values: []float64{
1,
},
},
}, },
{ {
Name: "2", Name: "2",
Value: 2, Value: EChartsSeriesDataValue{
values: []float64{
2,
},
},
}, },
}, },
}, },
@ -474,13 +482,13 @@ func TestEChartsSeriesList(t *testing.T) {
Type: ChartTypeBar, Type: ChartTypeBar,
Data: []EChartsSeriesData{ Data: []EChartsSeriesData{
{ {
Value: 1, Value: NewEChartsSeriesDataValue(1),
ItemStyle: EChartStyle{ ItemStyle: EChartStyle{
Color: "#aaa", Color: "#aaa",
}, },
}, },
{ {
Value: 2, Value: NewEChartsSeriesDataValue(2),
}, },
}, },
YAxisIndex: 1, YAxisIndex: 1,
@ -488,10 +496,10 @@ func TestEChartsSeriesList(t *testing.T) {
{ {
Data: []EChartsSeriesData{ Data: []EChartsSeriesData{
{ {
Value: 3, Value: NewEChartsSeriesDataValue(3),
}, },
{ {
Value: 4, Value: NewEChartsSeriesDataValue(4),
}, },
}, },
ItemStyle: EChartStyle{ ItemStyle: EChartStyle{

View file

@ -1483,6 +1483,75 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
} }
] ]
}`, }`,
`{
"title": {
"text": "Basic Radar Chart"
},
"legend": {
"data": [
"Allocated Budget",
"Actual Spending"
]
},
"radar": {
"indicator": [
{
"name": "Sales",
"max": 6500
},
{
"name": "Administration",
"max": 16000
},
{
"name": "Information Technology",
"max": 30000
},
{
"name": "Customer Support",
"max": 38000
},
{
"name": "Development",
"max": 52000
},
{
"name": "Marketing",
"max": 25000
}
]
},
"series": [
{
"name": "Budget vs spending",
"type": "radar",
"data": [
{
"value": [
4200,
3000,
20000,
35000,
50000,
18000
],
"name": "Allocated Budget"
},
{
"value": [
5000,
14000,
28000,
26000,
42000,
21000
],
"name": "Actual Spending"
}
]
}
]
}`,
`{ `{
"legend": { "legend": {
"top": "-140", "top": "-140",

32
line.go
View file

@ -55,25 +55,23 @@ func (d *Draw) lineFill(points []Point, style LineStyle) {
if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) { if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) {
return return
} }
r := d.Render
var x, y int newPoints := make([]Point, len(points))
s.GetFillOptions().WriteDrawingOptionsToRenderer(r) copy(newPoints, points)
for index, point := range points {
x = point.X
y = point.Y
if index == 0 {
d.moveTo(x, y)
} else {
d.lineTo(x, y)
}
}
height := d.Box.Height()
d.lineTo(x, height)
x0 := points[0].X x0 := points[0].X
y0 := points[0].Y y0 := points[0].Y
d.lineTo(x0, height) height := d.Box.Height()
d.lineTo(x0, y0) newPoints = append(newPoints, Point{
r.Fill() X: points[len(points)-1].X,
Y: height,
}, Point{
X: x0,
Y: height,
}, Point{
X: x0,
Y: y0,
})
d.fill(newPoints, style.Style())
} }
func (d *Draw) lineDot(points []Point, style LineStyle) { func (d *Draw) lineDot(points []Point, style LineStyle) {

View file

@ -30,12 +30,13 @@ import (
"github.com/wcharczuk/go-chart/v2/drawing" "github.com/wcharczuk/go-chart/v2/drawing"
) )
// 线 E0E6F1
// 填充 rgb(210,219,238) fill-opacity="0.2"
type RadarIndicator struct { type RadarIndicator struct {
// Indicator's name
Name string Name string
// The maximum value of indicator
Max float64 Max float64
// The minimum value of indicator
Min float64
} }
type radarChartOption struct { type radarChartOption struct {
@ -150,18 +151,19 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
// 雷达图 // 雷达图
angles := getPolygonPointAngles(sides) angles := getPolygonPointAngles(sides)
maxCount := len(opt.Indicators) maxCount := len(opt.Indicators)
for i, series := range opt.SeriesList { for _, series := range opt.SeriesList {
linePoints := make([]Point, 0, maxCount) linePoints := make([]Point, 0, maxCount)
for j, item := range series.Data { for j, item := range series.Data {
if j >= maxCount { if j >= maxCount {
continue continue
} }
percent := item.Value / opt.Indicators[j].Max indicator := opt.Indicators[j]
percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
r := percent * radius r := percent * radius
p := getPolygonPoint(center, r, angles[j]) p := getPolygonPoint(center, r, angles[j])
linePoints = append(linePoints, p) linePoints = append(linePoints, p)
} }
color := theme.GetSeriesColor(i) color := theme.GetSeriesColor(series.index)
dotFillColor := drawing.ColorWhite dotFillColor := drawing.ColorWhite
if theme.IsDark() { if theme.IsDark() {
dotFillColor = color dotFillColor = color
@ -176,7 +178,7 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
FillColor: color.WithAlpha(20), FillColor: color.WithAlpha(20),
} }
d.lineStroke(linePoints, s) d.lineStroke(linePoints, s)
d.lineFill(linePoints, s) d.fill(linePoints, s.Style())
d.lineDot(linePoints[0:len(linePoints)-1], s) d.lineDot(linePoints[0:len(linePoints)-1], s)
} }
return nil return nil

102
radar_chart_test.go Normal file

File diff suppressed because one or more lines are too long