feat: support radar option of echarts
This commit is contained in:
parent
570828d35f
commit
b93d096633
10 changed files with 411 additions and 56 deletions
|
|
@ -152,6 +152,11 @@ The name with `[]` is new parameter, others are the same as `echarts`.
|
|||
- `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` or`pie`
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
19
draw.go
19
draw.go
|
|
@ -322,3 +322,22 @@ func (d *Draw) polygon(center Point, radius float64, sides int) {
|
|||
d.lineTo(points[0].X, points[0].Y)
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
50
draw_test.go
50
draw_test.go
|
|
@ -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>",
|
||||
},
|
||||
// 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 {
|
||||
d, err := NewDraw(DrawOption{
|
||||
|
|
|
|||
69
echarts.go
69
echarts.go
|
|
@ -69,10 +69,30 @@ 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 {
|
||||
Value float64 `json:"value"`
|
||||
Name string `json:"name"`
|
||||
ItemStyle EChartStyle `json:"itemStyle"`
|
||||
Value EChartsSeriesDataValue `json:"value"`
|
||||
Name string `json:"name"`
|
||||
ItemStyle EChartStyle `json:"itemStyle"`
|
||||
}
|
||||
type _EChartsSeriesData EChartsSeriesData
|
||||
|
||||
|
|
@ -88,7 +108,11 @@ func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.Value = v
|
||||
es.Value = EChartsSeriesDataValue{
|
||||
values: []float64{
|
||||
v,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
v := _EChartsSeriesData{}
|
||||
|
|
@ -291,7 +315,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
|
|||
if item.Type == ChartTypePie {
|
||||
for _, dataItem := range item.Data {
|
||||
seriesList = append(seriesList, Series{
|
||||
Type: ChartTypePie,
|
||||
Type: item.Type,
|
||||
Name: dataItem.Name,
|
||||
Label: SeriesLabel{
|
||||
Show: true,
|
||||
|
|
@ -299,17 +323,28 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
|
|||
Radius: item.Radius,
|
||||
Data: []SeriesData{
|
||||
{
|
||||
Value: dataItem.Value,
|
||||
Value: dataItem.Value.First(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
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))
|
||||
for j, dataItem := range item.Data {
|
||||
data[j] = SeriesData{
|
||||
Value: dataItem.Value,
|
||||
Value: dataItem.Value.First(),
|
||||
Style: dataItem.ItemStyle.ToStyle(),
|
||||
}
|
||||
}
|
||||
|
|
@ -364,9 +399,12 @@ type EChartsOption struct {
|
|||
TextStyle EChartsTextStyle `json:"textStyle"`
|
||||
SubtextStyle EChartsTextStyle `json:"subtextStyle"`
|
||||
} `json:"title"`
|
||||
XAxis EChartsXAxis `json:"xAxis"`
|
||||
YAxis EChartsYAxis `json:"yAxis"`
|
||||
Legend EChartsLegend `json:"legend"`
|
||||
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"`
|
||||
}
|
||||
|
|
@ -397,11 +435,12 @@ func (eo *EChartsOption) ToOption() ChartOption {
|
|||
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(),
|
||||
RadarIndicators: eo.Radar.Indicator,
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -253,14 +253,14 @@ func TestEChartsSeriesData(t *testing.T) {
|
|||
err := esd.UnmarshalJSON([]byte(`123`))
|
||||
assert.Nil(err)
|
||||
assert.Equal(EChartsSeriesData{
|
||||
Value: 123,
|
||||
Value: NewEChartsSeriesDataValue(123),
|
||||
}, esd)
|
||||
|
||||
esd = EChartsSeriesData{}
|
||||
err = esd.UnmarshalJSON([]byte(`2.1`))
|
||||
assert.Nil(err)
|
||||
assert.Equal(EChartsSeriesData{
|
||||
Value: 2.1,
|
||||
Value: NewEChartsSeriesDataValue(2.1),
|
||||
}, esd)
|
||||
|
||||
esd = EChartsSeriesData{}
|
||||
|
|
@ -273,7 +273,7 @@ func TestEChartsSeriesData(t *testing.T) {
|
|||
}`))
|
||||
assert.Nil(err)
|
||||
assert.Equal(EChartsSeriesData{
|
||||
Value: 123.12,
|
||||
Value: NewEChartsSeriesDataValue(123.12),
|
||||
Name: "test",
|
||||
ItemStyle: EChartStyle{
|
||||
Color: "#aaa",
|
||||
|
|
@ -308,10 +308,10 @@ func TestEChartsSeries(t *testing.T) {
|
|||
Name: "Email",
|
||||
Data: []EChartsSeriesData{
|
||||
{
|
||||
Value: 120,
|
||||
Value: NewEChartsSeriesDataValue(120),
|
||||
},
|
||||
{
|
||||
Value: 132,
|
||||
Value: NewEChartsSeriesDataValue(132),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -320,10 +320,10 @@ func TestEChartsSeries(t *testing.T) {
|
|||
Type: "bar",
|
||||
Data: []EChartsSeriesData{
|
||||
{
|
||||
Value: 220,
|
||||
Value: NewEChartsSeriesDataValue(220),
|
||||
},
|
||||
{
|
||||
Value: 182,
|
||||
Value: NewEChartsSeriesDataValue(182),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -430,12 +430,20 @@ func TestEChartsSeriesList(t *testing.T) {
|
|||
Radius: "30%",
|
||||
Data: []EChartsSeriesData{
|
||||
{
|
||||
Name: "1",
|
||||
Value: 1,
|
||||
Name: "1",
|
||||
Value: EChartsSeriesDataValue{
|
||||
values: []float64{
|
||||
1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "2",
|
||||
Value: 2,
|
||||
Name: "2",
|
||||
Value: EChartsSeriesDataValue{
|
||||
values: []float64{
|
||||
2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -474,13 +482,13 @@ func TestEChartsSeriesList(t *testing.T) {
|
|||
Type: ChartTypeBar,
|
||||
Data: []EChartsSeriesData{
|
||||
{
|
||||
Value: 1,
|
||||
Value: NewEChartsSeriesDataValue(1),
|
||||
ItemStyle: EChartStyle{
|
||||
Color: "#aaa",
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: 2,
|
||||
Value: NewEChartsSeriesDataValue(2),
|
||||
},
|
||||
},
|
||||
YAxisIndex: 1,
|
||||
|
|
@ -488,10 +496,10 @@ func TestEChartsSeriesList(t *testing.T) {
|
|||
{
|
||||
Data: []EChartsSeriesData{
|
||||
{
|
||||
Value: 3,
|
||||
Value: NewEChartsSeriesDataValue(3),
|
||||
},
|
||||
{
|
||||
Value: 4,
|
||||
Value: NewEChartsSeriesDataValue(4),
|
||||
},
|
||||
},
|
||||
ItemStyle: EChartStyle{
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
"top": "-140",
|
||||
|
|
|
|||
32
line.go
32
line.go
|
|
@ -55,25 +55,23 @@ func (d *Draw) lineFill(points []Point, style LineStyle) {
|
|||
if !(s.ShouldDrawStroke() && 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)
|
||||
}
|
||||
}
|
||||
height := d.Box.Height()
|
||||
d.lineTo(x, height)
|
||||
|
||||
newPoints := make([]Point, len(points))
|
||||
copy(newPoints, points)
|
||||
x0 := points[0].X
|
||||
y0 := points[0].Y
|
||||
d.lineTo(x0, height)
|
||||
d.lineTo(x0, y0)
|
||||
r.Fill()
|
||||
height := d.Box.Height()
|
||||
newPoints = append(newPoints, Point{
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ import (
|
|||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
// 线 E0E6F1
|
||||
// 填充 rgb(210,219,238) fill-opacity="0.2"
|
||||
|
||||
type RadarIndicator struct {
|
||||
// Indicator's name
|
||||
Name string
|
||||
Max float64
|
||||
// The maximum value of indicator
|
||||
Max float64
|
||||
// The minimum value of indicator
|
||||
Min float64
|
||||
}
|
||||
|
||||
type radarChartOption struct {
|
||||
|
|
@ -150,18 +151,19 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
|||
// 雷达图
|
||||
angles := getPolygonPointAngles(sides)
|
||||
maxCount := len(opt.Indicators)
|
||||
for i, series := range opt.SeriesList {
|
||||
for _, series := range opt.SeriesList {
|
||||
linePoints := make([]Point, 0, maxCount)
|
||||
for j, item := range series.Data {
|
||||
if j >= maxCount {
|
||||
continue
|
||||
}
|
||||
percent := item.Value / opt.Indicators[j].Max
|
||||
indicator := opt.Indicators[j]
|
||||
percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
|
||||
r := percent * radius
|
||||
p := getPolygonPoint(center, r, angles[j])
|
||||
linePoints = append(linePoints, p)
|
||||
}
|
||||
color := theme.GetSeriesColor(i)
|
||||
color := theme.GetSeriesColor(series.index)
|
||||
dotFillColor := drawing.ColorWhite
|
||||
if theme.IsDark() {
|
||||
dotFillColor = color
|
||||
|
|
@ -176,7 +178,7 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
|||
FillColor: color.WithAlpha(20),
|
||||
}
|
||||
d.lineStroke(linePoints, s)
|
||||
d.lineFill(linePoints, s)
|
||||
d.fill(linePoints, s.Style())
|
||||
d.lineDot(linePoints[0:len(linePoints)-1], s)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
102
radar_chart_test.go
Normal file
102
radar_chart_test.go
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue