diff --git a/README.md b/README.md
index 30d9675..6cafc68 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[中文](./README_zh.md)
-`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and three themes: `light`, `dark` and `grafana`.
+`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`.
`Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`.
@@ -17,7 +17,7 @@ Screenshot of common charts, the left part is light theme, the right part is gra
## Chart Type
-Support three chart types: `line`, `bar` and `pie`.
+These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
## Example
@@ -159,7 +159,7 @@ The name with `[]` is new parameter, others are the same as `echarts`.
- `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`
+ - `series.type` Series type: `line`, `bar`, `pie`, `radar` or `funnel`
- `series.radius` Radius of Pie chart:`50%`, default is `40%`
- `series.yAxisIndex` Index of y axis to combine with, which is useful for multiple y axes in one chart
- `series.label.show` Whether to show label
diff --git a/README_zh.md b/README_zh.md
index f4f5d4a..e3747e7 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -3,7 +3,7 @@
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[](https://github.com/vicanso/go-charts/actions)
-`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持三种主题`light`, `dark`以及`grafana`。
+`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。
`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana):
@@ -14,7 +14,7 @@
## 支持图表类型
-暂仅支持三种的图表类型:`line`, `bar` 以及 `pie`
+支持以下的图表类型:`line`, `bar`, `pie`, `radar` 以及 `funnel`
## 示例
@@ -155,9 +155,14 @@ func main() {
- `legend.padding` legend的padding,配置方式与图表的`padding`一致
- `legend.left` legend离容器左侧的距离,其值可以为具体的像素值(20)或百分比(20%)、`left`或者`right`
- `legend.top` legend离容器顶部的距离,暂仅支持数值形式
+- `radar` 雷达图的坐标系
+ - `radar.indicator` 雷达图的指示器,用来指定雷达图中的多个变量(维度)
+ - `radar.indicator.name` 指示器名称
+ - `radar.indicator.max` 指示器的最大值,可选,建议设置
+ - `radar.indicator.min` 指示器的最小值,可选,默认为 0
- `series` 图表的数据项列表
- `series.name` 图表的名称,与`legend.data`对应,两者只只设置其一
- - `series.type` 图表的展示类型,暂支持`line`, `bar`以及`pie`,需要注意`pie`只能单独使用
+ - `series.type` 图表的展示类型,暂支持`line`, `bar`, `pie`, `radar` 以及 `funnel`。需要注意只有`line`与`bar`可以混用
- `series.radius` 饼图的半径值,如`50%`,默认为`40%`
- `series.yAxisIndex` 该数据项使用的y轴,默认为0,对yAxis的配置对应
- `series.label.show` 是否显示文本标签(默认为对应的值)
diff --git a/chart.go b/chart.go
index 2903051..e169387 100644
--- a/chart.go
+++ b/chart.go
@@ -25,6 +25,7 @@ package charts
import (
"errors"
"math"
+ "sort"
"strings"
"github.com/golang/freetype/truetype"
@@ -33,10 +34,11 @@ import (
)
const (
- ChartTypeLine = "line"
- ChartTypeBar = "bar"
- ChartTypePie = "pie"
- ChartTypeRadar = "radar"
+ ChartTypeLine = "line"
+ ChartTypeBar = "bar"
+ ChartTypePie = "pie"
+ ChartTypeRadar = "radar"
+ ChartTypeFunnel = "funnel"
)
const (
@@ -161,10 +163,19 @@ func (o *ChartOption) FillDefault(theme string) {
} else {
seriesCount := len(o.SeriesList)
for index, name := range o.Legend.Data {
- if index < seriesCount {
+ if index < seriesCount &&
+ len(o.SeriesList[index].Name) == 0 {
o.SeriesList[index].Name = name
}
}
+ nameIndexDict := map[string]int{}
+ for index, name := range o.Legend.Data {
+ nameIndexDict[name] = index
+ }
+ // 保证series的顺序与legend一致
+ sort.Slice(o.SeriesList, func(i, j int) bool {
+ return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name]
+ })
}
// 如果无legend数据,则隐藏
if len(strings.Join(o.Legend.Data, "")) == 0 {
@@ -289,13 +300,17 @@ func Render(opt ChartOption) (*Draw, error) {
barSeries := make([]Series, 0)
isPieChart := false
isRadarChart := false
- for index, item := range opt.SeriesList {
- item.index = index
+ isFunnelChart := false
+ for index := range opt.SeriesList {
+ opt.SeriesList[index].index = index
+ item := opt.SeriesList[index]
switch item.Type {
case ChartTypePie:
isPieChart = true
case ChartTypeRadar:
isRadarChart = true
+ case ChartTypeFunnel:
+ isFunnelChart = true
case ChartTypeBar:
barSeries = append(barSeries, item)
default:
@@ -305,7 +320,9 @@ func Render(opt ChartOption) (*Draw, error) {
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
// pie不需要axis
// radar 同样处理
- if isPieChart || isRadarChart {
+ if isPieChart ||
+ isRadarChart ||
+ isFunnelChart {
opt.XAxis.Hidden = true
for index := range opt.YAxisList {
opt.YAxisList[index].Hidden = true
@@ -340,6 +357,17 @@ func Render(opt ChartOption) (*Draw, error) {
Indicators: opt.RadarIndicators,
}, result)
},
+ // funnel render
+ func() error {
+ if !isFunnelChart {
+ return nil
+ }
+ return funnelChartRender(funnelChartOption{
+ SeriesList: opt.SeriesList,
+ Theme: opt.Theme,
+ Font: opt.Font,
+ }, result)
+ },
// bar render
func() error {
// 如果是pie或者无bar类型的series
diff --git a/echarts.go b/echarts.go
index 086dc45..fd38376 100644
--- a/echarts.go
+++ b/echarts.go
@@ -330,8 +330,9 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
}
continue
}
- // 如果是radar
- if item.Type == ChartTypeRadar {
+ // 如果是radar或funnel
+ if item.Type == ChartTypeRadar ||
+ item.Type == ChartTypeFunnel {
for _, dataItem := range item.Data {
seriesList = append(seriesList, Series{
Name: dataItem.Name,
diff --git a/examples/charts/main.go b/examples/charts/main.go
index 3ecb711..1d591fc 100644
--- a/examples/charts/main.go
+++ b/examples/charts/main.go
@@ -556,6 +556,57 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
},
},
+ // 漏斗图
+ {
+ Title: charts.TitleOption{
+ Text: "Funnel",
+ },
+ Legend: charts.NewLegendOption([]string{
+ "Show",
+ "Click",
+ "Visit",
+ "Inquiry",
+ "Order",
+ }),
+ SeriesList: []charts.Series{
+ {
+ Type: charts.ChartTypeFunnel,
+ Name: "Visit",
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 60,
+ }),
+ Max: charts.NewFloatPoint(120),
+ },
+ {
+ Type: charts.ChartTypeFunnel,
+ Name: "Inquiry",
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 40,
+ }),
+ },
+ {
+ Type: charts.ChartTypeFunnel,
+ Name: "Order",
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 20,
+ }),
+ },
+ {
+ Type: charts.ChartTypeFunnel,
+ Name: "Click",
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 80,
+ }),
+ },
+ {
+ Type: charts.ChartTypeFunnel,
+ Name: "Show",
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 100,
+ }),
+ },
+ },
+ },
// 多图展示
{
Legend: charts.LegendOption{
@@ -1552,6 +1603,91 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
}
]
}`,
+ `{
+ "title": {
+ "text": "Funnel"
+ },
+ "tooltip": {
+ "trigger": "item",
+ "formatter": "{a}
{b} : {c}%"
+ },
+ "toolbox": {
+ "feature": {
+ "dataView": {
+ "readOnly": false
+ },
+ "restore": {},
+ "saveAsImage": {}
+ }
+ },
+ "legend": {
+ "data": [
+ "Show",
+ "Click",
+ "Visit",
+ "Inquiry",
+ "Order"
+ ]
+ },
+ "series": [
+ {
+ "name": "Funnel",
+ "type": "funnel",
+ "left": "10%",
+ "top": 60,
+ "bottom": 60,
+ "width": "80%",
+ "min": 0,
+ "max": 100,
+ "minSize": "0%",
+ "maxSize": "100%",
+ "sort": "descending",
+ "gap": 2,
+ "label": {
+ "show": true,
+ "position": "inside"
+ },
+ "labelLine": {
+ "length": 10,
+ "lineStyle": {
+ "width": 1,
+ "type": "solid"
+ }
+ },
+ "itemStyle": {
+ "borderColor": "#fff",
+ "borderWidth": 1
+ },
+ "emphasis": {
+ "label": {
+ "fontSize": 20
+ }
+ },
+ "data": [
+ {
+ "value": 60,
+ "name": "Visit"
+ },
+ {
+ "value": 40,
+ "name": "Inquiry"
+ },
+ {
+ "value": 20,
+ "name": "Order"
+ },
+ {
+ "value": 80,
+ "name": "Click"
+ },
+ {
+ "value": 100,
+ "name": "Show"
+ }
+ ]
+ }
+ ]
+ }`,
`{
"legend": {
"top": "-140",
diff --git a/funnel.go b/funnel.go
new file mode 100644
index 0000000..2307fea
--- /dev/null
+++ b/funnel.go
@@ -0,0 +1,141 @@
+// 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 (
+ "fmt"
+ "sort"
+
+ "github.com/dustin/go-humanize"
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+type funnelChartOption struct {
+ Theme string
+ Font *truetype.Font
+ SeriesList SeriesList
+}
+
+func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error {
+ d, err := NewDraw(DrawOption{
+ Parent: result.d,
+ }, PaddingOption(chart.Box{
+ Top: result.titleBox.Height(),
+ }))
+ if err != nil {
+ return err
+ }
+ seriesList := make([]Series, len(opt.SeriesList))
+ copy(seriesList, opt.SeriesList)
+ sort.Slice(seriesList, func(i, j int) bool {
+ // 大的数据在前
+ return seriesList[i].Data[0].Value > seriesList[j].Data[0].Value
+ })
+ max := float64(100)
+ min := float64(0)
+ for _, item := range seriesList {
+ if item.Max != nil {
+ max = *item.Max
+ }
+ if item.Min != nil {
+ min = *item.Min
+ }
+ }
+
+ theme := NewTheme(opt.Theme)
+ gap := 2
+ height := d.Box.Height()
+ width := d.Box.Width()
+ count := len(seriesList)
+
+ h := (height - gap*(count-1)) / count
+
+ y := 0
+ widthList := make([]int, len(seriesList))
+ textList := make([]string, len(seriesList))
+ for index, item := range seriesList {
+ value := item.Data[0].Value
+ percent := (item.Data[0].Value - min) / (max - min)
+ w := int(percent * float64(width))
+ widthList[index] = w
+ p := humanize.CommafWithDigits(value, 2) + "%"
+ textList[index] = fmt.Sprintf("%s(%s)", item.Name, p)
+ }
+
+ for index, w := range widthList {
+ series := seriesList[index]
+ nextWidth := 0
+ if index+1 < len(widthList) {
+ nextWidth = widthList[index+1]
+ }
+ topStartX := (width - w) >> 1
+ topEndX := topStartX + w
+ bottomStartX := (width - nextWidth) >> 1
+ bottomEndX := bottomStartX + nextWidth
+ points := []Point{
+ {
+ X: topStartX,
+ Y: y,
+ },
+ {
+ X: topEndX,
+ Y: y,
+ },
+ {
+ X: bottomEndX,
+ Y: y + h,
+ },
+ {
+ X: bottomStartX,
+ Y: y + h,
+ },
+ {
+ X: topStartX,
+ Y: y,
+ },
+ }
+ color := theme.GetSeriesColor(series.index)
+ d.fill(points, chart.Style{
+ FillColor: color,
+ })
+
+ // 文本
+ text := textList[index]
+ r := d.Render
+ textStyle := chart.Style{
+ FontColor: theme.GetTextColor(),
+ FontSize: labelFontSize,
+ Font: opt.Font,
+ }
+ textStyle.GetTextOptions().WriteToRenderer(r)
+ textBox := r.MeasureText(text)
+ textX := width>>1 - textBox.Width()>>1
+ textY := y + h>>1
+ d.text(text, textX, textY)
+
+ y += (h + gap)
+ }
+
+ return nil
+}
diff --git a/funnel_test.go b/funnel_test.go
new file mode 100644
index 0000000..530fa53
--- /dev/null
+++ b/funnel_test.go
@@ -0,0 +1,91 @@
+// 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 TestFunnelChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 250,
+ Height: 150,
+ })
+ assert.Nil(err)
+ f, _ := chart.GetDefaultFont()
+ err = funnelChartRender(funnelChartOption{
+ Font: f,
+ SeriesList: []Series{
+ {
+ Type: ChartTypeFunnel,
+ Name: "Visit",
+ Data: NewSeriesDataFromValues([]float64{
+ 60,
+ }),
+ },
+ {
+ Type: ChartTypeFunnel,
+ Name: "Inquiry",
+ Data: NewSeriesDataFromValues([]float64{
+ 40,
+ }),
+ index: 1,
+ },
+ {
+ Type: ChartTypeFunnel,
+ Name: "Order",
+ Data: NewSeriesDataFromValues([]float64{
+ 20,
+ }),
+ index: 2,
+ },
+ {
+ Type: ChartTypeFunnel,
+ Name: "Click",
+ Data: NewSeriesDataFromValues([]float64{
+ 80,
+ }),
+ index: 3,
+ },
+ {
+ Type: ChartTypeFunnel,
+ Name: "Show",
+ Data: NewSeriesDataFromValues([]float64{
+ 100,
+ }),
+ index: 4,
+ },
+ },
+ }, &basicRenderResult{
+ d: d,
+ })
+ assert.Nil(err)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/radar_chart.go b/radar_chart.go
index c0f61b0..364213d 100644
--- a/radar_chart.go
+++ b/radar_chart.go
@@ -51,9 +51,17 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
if sides < 3 {
return errors.New("The count of indicator should be >= 3")
}
- for _, indicator := range opt.Indicators {
+ maxValues := make([]float64, len(opt.Indicators))
+ for _, series := range opt.SeriesList {
+ for index, item := range series.Data {
+ if index < len(maxValues) && item.Value > maxValues[index] {
+ maxValues[index] = item.Value
+ }
+ }
+ }
+ for index, indicator := range opt.Indicators {
if indicator.Max <= 0 {
- return errors.New("The max of indicator should be > 0")
+ opt.Indicators[index].Max = maxValues[index]
}
}
d, err := NewDraw(DrawOption{
diff --git a/series.go b/series.go
index 8a9ba73..a1b7486 100644
--- a/series.go
+++ b/series.go
@@ -115,6 +115,10 @@ type Series struct {
MarkPoint SeriesMarkPoint
// Make line for series
MarkLine SeriesMarkLine
+ // Max value of series
+ Min *float64
+ // Min value of series
+ Max *float64
}
type SeriesList []Series