diff --git a/README.md b/README.md
index 6710cdd..30d9675 100644
--- a/README.md
+++ b/README.md
@@ -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`
diff --git a/chart_test.go b/chart_test.go
index 4fc3d20..1f4d1f4 100644
--- a/chart_test.go
+++ b/chart_test.go
@@ -270,12 +270,75 @@ func TestChartRender(t *testing.T) {
Radius: "35%",
}),
},
+ {
+ Legend: NewLegendOption([]string{
+ "Allocated Budget",
+ "Actual Spending",
+ }),
+ Box: chart.Box{
+ Top: 20,
+ Left: 0,
+ Right: 200,
+ Bottom: 120,
+ },
+ RadarIndicators: []RadarIndicator{
+ {
+ 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,
+ },
+ },
+ SeriesList: SeriesList{
+ {
+ Type: ChartTypeRadar,
+ Data: NewSeriesDataFromValues([]float64{
+ 4200,
+ 3000,
+ 20000,
+ 35000,
+ 50000,
+ 18000,
+ }),
+ },
+ {
+ Type: ChartTypeRadar,
+ index: 1,
+ Data: NewSeriesDataFromValues([]float64{
+ 5000,
+ 14000,
+ 28000,
+ 26000,
+ 42000,
+ 21000,
+ }),
+ },
+ },
+ },
},
})
assert.Nil(err)
data, err := d.Bytes()
assert.Nil(err)
- assert.Equal("", string(data))
+ assert.Equal("", string(data))
}
func BenchmarkMultiChartPNGRender(b *testing.B) {
diff --git a/draw.go b/draw.go
index ae52f05..f6ee73c 100644
--- a/draw.go
+++ b/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()
+}
diff --git a/draw_test.go b/draw_test.go
index 712641a..694d72a 100644
--- a/draw_test.go
+++ b/draw_test.go
@@ -409,6 +409,56 @@ func TestDraw(t *testing.T) {
},
result: "",
},
+ // 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: "",
+ },
+ // 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: "",
+ },
}
for _, tt := range tests {
d, err := NewDraw(DrawOption{
diff --git a/echarts.go b/echarts.go
index 926ab5f..086dc45 100644
--- a/echarts.go
+++ b/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]
diff --git a/echarts_test.go b/echarts_test.go
index f3b70f1..05c2a40 100644
--- a/echarts_test.go
+++ b/echarts_test.go
@@ -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{
diff --git a/examples/charts/main.go b/examples/charts/main.go
index b175126..3ecb711 100644
--- a/examples/charts/main.go
+++ b/examples/charts/main.go
@@ -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",
diff --git a/line.go b/line.go
index e1f3583..0fc25d6 100644
--- a/line.go
+++ b/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) {
diff --git a/radar_chart.go b/radar_chart.go
index 667e3a0..c0f61b0 100644
--- a/radar_chart.go
+++ b/radar_chart.go
@@ -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
diff --git a/radar_chart_test.go b/radar_chart_test.go
new file mode 100644
index 0000000..c5d2aa9
--- /dev/null
+++ b/radar_chart_test.go
@@ -0,0 +1,102 @@
+// 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"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+func TestRadarChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 250,
+ Height: 150,
+ })
+ assert.Nil(err)
+
+ f, _ := chart.GetDefaultFont()
+ err = radarChartRender(radarChartOption{
+ Font: f,
+ Indicators: []RadarIndicator{
+ {
+ 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,
+ },
+ },
+ SeriesList: SeriesList{
+ {
+ Type: ChartTypeRadar,
+ Data: NewSeriesDataFromValues([]float64{
+ 4200,
+ 3000,
+ 20000,
+ 35000,
+ 50000,
+ 18000,
+ }),
+ },
+ {
+ Type: ChartTypeRadar,
+ index: 1,
+ Data: NewSeriesDataFromValues([]float64{
+ 5000,
+ 14000,
+ 28000,
+ 26000,
+ 42000,
+ 21000,
+ }),
+ },
+ },
+ }, &basicRenderResult{
+ d: d,
+ })
+ assert.Nil(err)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}