feat: support funnel chart
This commit is contained in:
parent
b93d096633
commit
5519d2eca6
9 changed files with 432 additions and 18 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[中文](./README_zh.md)
|
[中文](./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`.
|
`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
|
## Chart Type
|
||||||
|
|
||||||
Support three chart types: `line`, `bar` and `pie`.
|
These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
|
||||||
|
|
||||||
## Example
|
## 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.
|
- `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`, `pie`, `radar` or `funnel`
|
||||||
- `series.radius` Radius of Pie chart:`50%`, default is `40%`
|
- `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.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
|
- `series.label.show` Whether to show label
|
||||||
|
|
|
||||||
11
README_zh.md
11
README_zh.md
|
|
@ -3,7 +3,7 @@
|
||||||
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
|
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
|
||||||
[](https://github.com/vicanso/go-charts/actions)
|
[](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):
|
`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.padding` legend的padding,配置方式与图表的`padding`一致
|
||||||
- `legend.left` legend离容器左侧的距离,其值可以为具体的像素值(20)或百分比(20%)、`left`或者`right`
|
- `legend.left` legend离容器左侧的距离,其值可以为具体的像素值(20)或百分比(20%)、`left`或者`right`
|
||||||
- `legend.top` legend离容器顶部的距离,暂仅支持数值形式
|
- `legend.top` legend离容器顶部的距离,暂仅支持数值形式
|
||||||
|
- `radar` 雷达图的坐标系
|
||||||
|
- `radar.indicator` 雷达图的指示器,用来指定雷达图中的多个变量(维度)
|
||||||
|
- `radar.indicator.name` 指示器名称
|
||||||
|
- `radar.indicator.max` 指示器的最大值,可选,建议设置
|
||||||
|
- `radar.indicator.min` 指示器的最小值,可选,默认为 0
|
||||||
- `series` 图表的数据项列表
|
- `series` 图表的数据项列表
|
||||||
- `series.name` 图表的名称,与`legend.data`对应,两者只只设置其一
|
- `series.name` 图表的名称,与`legend.data`对应,两者只只设置其一
|
||||||
- `series.type` 图表的展示类型,暂支持`line`, `bar`以及`pie`,需要注意`pie`只能单独使用
|
- `series.type` 图表的展示类型,暂支持`line`, `bar`, `pie`, `radar` 以及 `funnel`。需要注意只有`line`与`bar`可以混用
|
||||||
- `series.radius` 饼图的半径值,如`50%`,默认为`40%`
|
- `series.radius` 饼图的半径值,如`50%`,默认为`40%`
|
||||||
- `series.yAxisIndex` 该数据项使用的y轴,默认为0,对yAxis的配置对应
|
- `series.yAxisIndex` 该数据项使用的y轴,默认为0,对yAxis的配置对应
|
||||||
- `series.label.show` 是否显示文本标签(默认为对应的值)
|
- `series.label.show` 是否显示文本标签(默认为对应的值)
|
||||||
|
|
|
||||||
36
chart.go
36
chart.go
|
|
@ -25,6 +25,7 @@ package charts
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
|
|
@ -37,6 +38,7 @@ const (
|
||||||
ChartTypeBar = "bar"
|
ChartTypeBar = "bar"
|
||||||
ChartTypePie = "pie"
|
ChartTypePie = "pie"
|
||||||
ChartTypeRadar = "radar"
|
ChartTypeRadar = "radar"
|
||||||
|
ChartTypeFunnel = "funnel"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -161,10 +163,19 @@ func (o *ChartOption) FillDefault(theme string) {
|
||||||
} else {
|
} else {
|
||||||
seriesCount := len(o.SeriesList)
|
seriesCount := len(o.SeriesList)
|
||||||
for index, name := range o.Legend.Data {
|
for index, name := range o.Legend.Data {
|
||||||
if index < seriesCount {
|
if index < seriesCount &&
|
||||||
|
len(o.SeriesList[index].Name) == 0 {
|
||||||
o.SeriesList[index].Name = name
|
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数据,则隐藏
|
// 如果无legend数据,则隐藏
|
||||||
if len(strings.Join(o.Legend.Data, "")) == 0 {
|
if len(strings.Join(o.Legend.Data, "")) == 0 {
|
||||||
|
|
@ -289,13 +300,17 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
barSeries := make([]Series, 0)
|
barSeries := make([]Series, 0)
|
||||||
isPieChart := false
|
isPieChart := false
|
||||||
isRadarChart := false
|
isRadarChart := false
|
||||||
for index, item := range opt.SeriesList {
|
isFunnelChart := false
|
||||||
item.index = index
|
for index := range opt.SeriesList {
|
||||||
|
opt.SeriesList[index].index = index
|
||||||
|
item := opt.SeriesList[index]
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case ChartTypePie:
|
case ChartTypePie:
|
||||||
isPieChart = true
|
isPieChart = true
|
||||||
case ChartTypeRadar:
|
case ChartTypeRadar:
|
||||||
isRadarChart = true
|
isRadarChart = true
|
||||||
|
case ChartTypeFunnel:
|
||||||
|
isFunnelChart = true
|
||||||
case ChartTypeBar:
|
case ChartTypeBar:
|
||||||
barSeries = append(barSeries, item)
|
barSeries = append(barSeries, item)
|
||||||
default:
|
default:
|
||||||
|
|
@ -305,7 +320,9 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
||||||
// pie不需要axis
|
// pie不需要axis
|
||||||
// radar 同样处理
|
// radar 同样处理
|
||||||
if isPieChart || isRadarChart {
|
if isPieChart ||
|
||||||
|
isRadarChart ||
|
||||||
|
isFunnelChart {
|
||||||
opt.XAxis.Hidden = true
|
opt.XAxis.Hidden = true
|
||||||
for index := range opt.YAxisList {
|
for index := range opt.YAxisList {
|
||||||
opt.YAxisList[index].Hidden = true
|
opt.YAxisList[index].Hidden = true
|
||||||
|
|
@ -340,6 +357,17 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
Indicators: opt.RadarIndicators,
|
Indicators: opt.RadarIndicators,
|
||||||
}, result)
|
}, result)
|
||||||
},
|
},
|
||||||
|
// funnel render
|
||||||
|
func() error {
|
||||||
|
if !isFunnelChart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return funnelChartRender(funnelChartOption{
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Font: opt.Font,
|
||||||
|
}, result)
|
||||||
|
},
|
||||||
// bar render
|
// bar render
|
||||||
func() error {
|
func() error {
|
||||||
// 如果是pie或者无bar类型的series
|
// 如果是pie或者无bar类型的series
|
||||||
|
|
|
||||||
|
|
@ -330,8 +330,9 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 如果是radar
|
// 如果是radar或funnel
|
||||||
if item.Type == ChartTypeRadar {
|
if item.Type == ChartTypeRadar ||
|
||||||
|
item.Type == ChartTypeFunnel {
|
||||||
for _, dataItem := range item.Data {
|
for _, dataItem := range item.Data {
|
||||||
seriesList = append(seriesList, Series{
|
seriesList = append(seriesList, Series{
|
||||||
Name: dataItem.Name,
|
Name: dataItem.Name,
|
||||||
|
|
|
||||||
|
|
@ -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{
|
Legend: charts.LegendOption{
|
||||||
|
|
@ -1552,6 +1603,91 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`,
|
}`,
|
||||||
|
`{
|
||||||
|
"title": {
|
||||||
|
"text": "Funnel"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": "item",
|
||||||
|
"formatter": "{a} <br/>{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": {
|
"legend": {
|
||||||
"top": "-140",
|
"top": "-140",
|
||||||
|
|
|
||||||
141
funnel.go
Normal file
141
funnel.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
91
funnel_test.go
Normal file
91
funnel_test.go
Normal file
|
|
@ -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("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"250\" height=\"150\">\\n<path d=\"M 0 0\nL 250 0\nL 225 28\nL 25 28\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(115,192,222,1.0)\"/><text x=\"89\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Show(100%)</text><path d=\"M 25 30\nL 225 30\nL 200 58\nL 50 58\nL 25 30\" style=\"stroke-width:0;stroke:none;fill:rgba(238,102,102,1.0)\"/><text x=\"94\" y=\"44\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Click(80%)</text><path d=\"M 50 60\nL 200 60\nL 175 88\nL 75 88\nL 50 60\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/><text x=\"96\" y=\"74\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Visit(60%)</text><path d=\"M 75 90\nL 175 90\nL 150 118\nL 100 118\nL 75 90\" style=\"stroke-width:0;stroke:none;fill:rgba(145,204,117,1.0)\"/><text x=\"89\" y=\"104\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Inquiry(40%)</text><path d=\"M 100 120\nL 150 120\nL 125 148\nL 125 148\nL 100 120\" style=\"stroke-width:0;stroke:none;fill:rgba(250,200,88,1.0)\"/><text x=\"93\" y=\"134\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Order(20%)</text></svg>", string(data))
|
||||||
|
}
|
||||||
|
|
@ -51,9 +51,17 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
||||||
if sides < 3 {
|
if sides < 3 {
|
||||||
return errors.New("The count of indicator should be >= 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 {
|
if indicator.Max <= 0 {
|
||||||
return errors.New("The max of indicator should be > 0")
|
opt.Indicators[index].Max = maxValues[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ type Series struct {
|
||||||
MarkPoint SeriesMarkPoint
|
MarkPoint SeriesMarkPoint
|
||||||
// Make line for series
|
// Make line for series
|
||||||
MarkLine SeriesMarkLine
|
MarkLine SeriesMarkLine
|
||||||
|
// Max value of series
|
||||||
|
Min *float64
|
||||||
|
// Min value of series
|
||||||
|
Max *float64
|
||||||
}
|
}
|
||||||
type SeriesList []Series
|
type SeriesList []Series
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue