feat: support pie, radar and funnel chart
This commit is contained in:
parent
3f24521593
commit
65a1cb11ad
18 changed files with 1987 additions and 85 deletions
34
axis.go
34
axis.go
|
|
@ -23,7 +23,10 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type axisPainter struct {
|
type axisPainter struct {
|
||||||
|
|
@ -41,11 +44,15 @@ func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter {
|
||||||
type AxisOption struct {
|
type AxisOption struct {
|
||||||
// The theme of chart
|
// The theme of chart
|
||||||
Theme ColorPalette
|
Theme ColorPalette
|
||||||
|
// Formatter for y axis text value
|
||||||
|
Formatter string
|
||||||
// The label of axis
|
// The label of axis
|
||||||
Data []string
|
Data []string
|
||||||
// The boundary gap on both sides of a coordinate axis.
|
// The boundary gap on both sides of a coordinate axis.
|
||||||
// Nil or *true means the center part of two axis ticks
|
// Nil or *true means the center part of two axis ticks
|
||||||
BoundaryGap *bool
|
BoundaryGap *bool
|
||||||
|
// The flag for show axis, set this to *false will hide axis
|
||||||
|
Show *bool
|
||||||
// The position of axis, it can be 'left', 'top', 'right' or 'bottom'
|
// The position of axis, it can be 'left', 'top', 'right' or 'bottom'
|
||||||
Position string
|
Position string
|
||||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||||
|
|
@ -74,6 +81,9 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
opt := a.opt
|
opt := a.opt
|
||||||
top := a.p
|
top := a.p
|
||||||
theme := opt.Theme
|
theme := opt.Theme
|
||||||
|
if opt.Show != nil && !*opt.Show {
|
||||||
|
return BoxZero, nil
|
||||||
|
}
|
||||||
|
|
||||||
strokeWidth := opt.StrokeWidth
|
strokeWidth := opt.StrokeWidth
|
||||||
if strokeWidth == 0 {
|
if strokeWidth == 0 {
|
||||||
|
|
@ -97,10 +107,15 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
strokeColor = theme.GetAxisStrokeColor()
|
strokeColor = theme.GetAxisStrokeColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
tickCount := opt.SplitNumber
|
data := opt.Data
|
||||||
if tickCount == 0 {
|
formatter := opt.Formatter
|
||||||
tickCount = len(opt.Data)
|
if len(formatter) != 0 {
|
||||||
|
for index, text := range data {
|
||||||
|
data[index] = strings.ReplaceAll(formatter, "{value}", text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
dataCount := len(data)
|
||||||
|
tickCount := dataCount
|
||||||
|
|
||||||
boundaryGap := true
|
boundaryGap := true
|
||||||
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
|
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
|
||||||
|
|
@ -118,8 +133,6 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
labelPosition = PositionCenter
|
labelPosition = PositionCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 计算unit
|
|
||||||
unit := 1
|
|
||||||
// 如果小于0,则表示不处理
|
// 如果小于0,则表示不处理
|
||||||
tickLength := getDefaultInt(opt.TickLength, 5)
|
tickLength := getDefaultInt(opt.TickLength, 5)
|
||||||
labelMargin := getDefaultInt(opt.LabelMargin, 5)
|
labelMargin := getDefaultInt(opt.LabelMargin, 5)
|
||||||
|
|
@ -133,7 +146,9 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
}
|
}
|
||||||
top.SetDrawingStyle(style).OverrideTextStyle(style)
|
top.SetDrawingStyle(style).OverrideTextStyle(style)
|
||||||
|
|
||||||
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data)
|
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
|
||||||
|
textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth))
|
||||||
|
unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber)))
|
||||||
|
|
||||||
width := 0
|
width := 0
|
||||||
height := 0
|
height := 0
|
||||||
|
|
@ -226,7 +241,7 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
Right: labelPaddingRight,
|
Right: labelPaddingRight,
|
||||||
})).MultiText(MultiTextOption{
|
})).MultiText(MultiTextOption{
|
||||||
Align: textAlign,
|
Align: textAlign,
|
||||||
TextList: opt.Data,
|
TextList: data,
|
||||||
Orient: orient,
|
Orient: orient,
|
||||||
Unit: unit,
|
Unit: unit,
|
||||||
Position: labelPosition,
|
Position: labelPosition,
|
||||||
|
|
@ -242,10 +257,7 @@ func (a *axisPainter) Render() (Box, error) {
|
||||||
x0 = 0
|
x0 = 0
|
||||||
x1 = top.Width() - p.Width()
|
x1 = top.Width() - p.Width()
|
||||||
}
|
}
|
||||||
for index, y := range autoDivide(height, tickCount) {
|
for _, y := range autoDivide(height, tickCount) {
|
||||||
if index == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
top.LineStroke([]Point{
|
top.LineStroke([]Point{
|
||||||
{
|
{
|
||||||
X: x0,
|
X: x0,
|
||||||
|
|
|
||||||
16
bar_chart.go
16
bar_chart.go
|
|
@ -95,14 +95,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
||||||
markPointPainter,
|
markPointPainter,
|
||||||
markLinePainter,
|
markLinePainter,
|
||||||
}
|
}
|
||||||
for i := range seriesList {
|
for index := range seriesList {
|
||||||
series := seriesList[i]
|
series := seriesList[index]
|
||||||
yRange := result.axisRanges[series.AxisIndex]
|
yRange := result.axisRanges[series.AxisIndex]
|
||||||
index := series.index
|
seriesColor := theme.GetSeriesColor(series.index)
|
||||||
if index == 0 {
|
|
||||||
index = i
|
|
||||||
}
|
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
|
||||||
|
|
||||||
divideValues := xRange.AutoDivide()
|
divideValues := xRange.AutoDivide()
|
||||||
points := make([]Point, len(series.Data))
|
points := make([]Point, len(series.Data))
|
||||||
|
|
@ -112,8 +108,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
||||||
}
|
}
|
||||||
x := divideValues[j]
|
x := divideValues[j]
|
||||||
x += margin
|
x += margin
|
||||||
if i != 0 {
|
if index != 0 {
|
||||||
x += i * (barWidth + barMargin)
|
x += index * (barWidth + barMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := int(yRange.getHeight(item.Value))
|
h := int(yRange.getHeight(item.Value))
|
||||||
|
|
@ -151,7 +147,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
||||||
if distance == 0 {
|
if distance == 0 {
|
||||||
distance = 5
|
distance = 5
|
||||||
}
|
}
|
||||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1)
|
||||||
labelStyle := Style{
|
labelStyle := Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ type ChartOption struct {
|
||||||
// The series list
|
// The series list
|
||||||
SeriesList SeriesList
|
SeriesList SeriesList
|
||||||
// The radar indicator list
|
// The radar indicator list
|
||||||
// RadarIndicators []RadarIndicator
|
RadarIndicators []RadarIndicator
|
||||||
// The background color of chart
|
// The background color of chart
|
||||||
BackgroundColor Color
|
BackgroundColor Color
|
||||||
// The child charts
|
// The child charts
|
||||||
|
|
|
||||||
150
charts.go
150
charts.go
|
|
@ -22,7 +22,10 @@
|
||||||
|
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
const labelFontSize = 10
|
const labelFontSize = 10
|
||||||
const defaultDotWidth = 2.0
|
const defaultDotWidth = 2.0
|
||||||
|
|
@ -140,16 +143,29 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
||||||
if containsInt(axisIndexList, series.AxisIndex) {
|
if containsInt(axisIndexList, series.AxisIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
axisIndexList = append(axisIndexList, series.index)
|
axisIndexList = append(axisIndexList, series.AxisIndex)
|
||||||
}
|
}
|
||||||
// 高度需要减去x轴的高度
|
// 高度需要减去x轴的高度
|
||||||
rangeHeight := p.Height() - defaultXAxisHeight
|
rangeHeight := p.Height() - defaultXAxisHeight
|
||||||
rangeWidthLeft := 0
|
rangeWidthLeft := 0
|
||||||
rangeWidthRight := 0
|
rangeWidthRight := 0
|
||||||
|
|
||||||
|
// 倒序
|
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList)))
|
||||||
|
|
||||||
// 计算对应的axis range
|
// 计算对应的axis range
|
||||||
for _, index := range axisIndexList {
|
for _, index := range axisIndexList {
|
||||||
|
yAxisOption := YAxisOption{}
|
||||||
|
if len(opt.YAxisOptions) > index {
|
||||||
|
yAxisOption = opt.YAxisOptions[index]
|
||||||
|
}
|
||||||
max, min := opt.SeriesList.GetMaxMin(index)
|
max, min := opt.SeriesList.GetMaxMin(index)
|
||||||
|
if yAxisOption.Min != nil {
|
||||||
|
min = *yAxisOption.Min
|
||||||
|
}
|
||||||
|
if yAxisOption.Max != nil {
|
||||||
|
max = *yAxisOption.Max
|
||||||
|
}
|
||||||
r := NewRange(AxisRangeOption{
|
r := NewRange(AxisRangeOption{
|
||||||
Min: min,
|
Min: min,
|
||||||
Max: max,
|
Max: max,
|
||||||
|
|
@ -159,10 +175,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
||||||
DivideCount: defaultAxisDivideCount,
|
DivideCount: defaultAxisDivideCount,
|
||||||
})
|
})
|
||||||
result.axisRanges[index] = r
|
result.axisRanges[index] = r
|
||||||
yAxisOption := YAxisOption{}
|
|
||||||
if len(opt.YAxisOptions) > index {
|
|
||||||
yAxisOption = opt.YAxisOptions[index]
|
|
||||||
}
|
|
||||||
if yAxisOption.Theme == nil {
|
if yAxisOption.Theme == nil {
|
||||||
yAxisOption.Theme = opt.Theme
|
yAxisOption.Theme = opt.Theme
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +188,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
||||||
}
|
}
|
||||||
reverseStringSlice(yAxisOption.Data)
|
reverseStringSlice(yAxisOption.Data)
|
||||||
// TODO生成其它位置既yAxis
|
// TODO生成其它位置既yAxis
|
||||||
yAxis := NewLeftYAxis(p, yAxisOption)
|
var yAxis *axisPainter
|
||||||
|
child := p.Child(PainterPaddingOption(Box{
|
||||||
|
Left: rangeWidthLeft,
|
||||||
|
Right: rangeWidthRight,
|
||||||
|
}))
|
||||||
|
if index == 0 {
|
||||||
|
yAxis = NewLeftYAxis(child, yAxisOption)
|
||||||
|
} else {
|
||||||
|
yAxis = NewRightYAxis(child, yAxisOption)
|
||||||
|
}
|
||||||
yAxisBox, err := yAxis.Render()
|
yAxisBox, err := yAxis.Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -191,7 +213,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
||||||
opt.XAxis.Theme = opt.Theme
|
opt.XAxis.Theme = opt.Theme
|
||||||
}
|
}
|
||||||
xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
|
xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
|
||||||
Left: rangeWidthLeft,
|
Left: rangeWidthLeft,
|
||||||
|
Right: rangeWidthRight,
|
||||||
})), opt.XAxis)
|
})), opt.XAxis)
|
||||||
_, err := xAxis.Render()
|
_, err := xAxis.Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -219,7 +242,9 @@ func doRender(renderers ...Renderer) error {
|
||||||
func Render(opt ChartOption) (*Painter, error) {
|
func Render(opt ChartOption) (*Painter, error) {
|
||||||
opt.fillDefault()
|
opt.fillDefault()
|
||||||
|
|
||||||
|
isChild := true
|
||||||
if opt.Parent == nil {
|
if opt.Parent == nil {
|
||||||
|
isChild = false
|
||||||
p, err := NewPainter(PainterOptions{
|
p, err := NewPainter(PainterOptions{
|
||||||
Type: opt.Type,
|
Type: opt.Type,
|
||||||
Width: opt.Width,
|
Width: opt.Width,
|
||||||
|
|
@ -231,21 +256,40 @@ func Render(opt ChartOption) (*Painter, error) {
|
||||||
opt.Parent = p
|
opt.Parent = p
|
||||||
}
|
}
|
||||||
p := opt.Parent
|
p := opt.Parent
|
||||||
p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
|
if !opt.Box.IsZero() {
|
||||||
|
p = p.Child(PainterBoxOption(opt.Box))
|
||||||
|
}
|
||||||
|
if !isChild {
|
||||||
|
p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
|
||||||
|
}
|
||||||
seriesList := opt.SeriesList
|
seriesList := opt.SeriesList
|
||||||
seriesList.init()
|
seriesList.init()
|
||||||
|
|
||||||
|
seriesCount := len(seriesList)
|
||||||
|
|
||||||
// line chart
|
// line chart
|
||||||
lineSeriesList := seriesList.Filter(ChartTypeLine)
|
lineSeriesList := seriesList.Filter(ChartTypeLine)
|
||||||
barSeriesList := seriesList.Filter(ChartTypeBar)
|
barSeriesList := seriesList.Filter(ChartTypeBar)
|
||||||
horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar)
|
horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar)
|
||||||
if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != len(seriesList) {
|
pieSeriesList := seriesList.Filter(ChartTypePie)
|
||||||
|
radarSeriesList := seriesList.Filter(ChartTypeRadar)
|
||||||
|
funnelSeriesList := seriesList.Filter(ChartTypeFunnel)
|
||||||
|
|
||||||
|
if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount {
|
||||||
return nil, errors.New("Horizontal bar can not mix other charts")
|
return nil, errors.New("Horizontal bar can not mix other charts")
|
||||||
}
|
}
|
||||||
|
if len(pieSeriesList) != 0 && len(pieSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Pie can not mix other charts")
|
||||||
|
}
|
||||||
|
if len(radarSeriesList) != 0 && len(radarSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Radar can not mix other charts")
|
||||||
|
}
|
||||||
|
if len(funnelSeriesList) != 0 && len(funnelSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Funnel can not mix other charts")
|
||||||
|
}
|
||||||
|
|
||||||
axisReversed := len(horizontalBarSeriesList) != 0
|
axisReversed := len(horizontalBarSeriesList) != 0
|
||||||
|
renderOpt := defaultRenderOption{
|
||||||
renderResult, err := defaultRender(p, defaultRenderOption{
|
|
||||||
Theme: opt.theme,
|
Theme: opt.theme,
|
||||||
Padding: opt.Padding,
|
Padding: opt.Padding,
|
||||||
SeriesList: opt.SeriesList,
|
SeriesList: opt.SeriesList,
|
||||||
|
|
@ -254,24 +298,28 @@ func Render(opt ChartOption) (*Painter, error) {
|
||||||
TitleOption: opt.Title,
|
TitleOption: opt.Title,
|
||||||
LegendOption: opt.Legend,
|
LegendOption: opt.Legend,
|
||||||
axisReversed: axisReversed,
|
axisReversed: axisReversed,
|
||||||
})
|
}
|
||||||
|
if isChild {
|
||||||
|
renderOpt.backgroundIsFilled = true
|
||||||
|
}
|
||||||
|
if len(pieSeriesList) != 0 ||
|
||||||
|
len(radarSeriesList) != 0 ||
|
||||||
|
len(funnelSeriesList) != 0 {
|
||||||
|
renderOpt.XAxis.Show = FalseFlag()
|
||||||
|
renderOpt.YAxisOptions = []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResult, err := defaultRender(p, renderOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := renderHandler{}
|
handler := renderHandler{}
|
||||||
|
|
||||||
if len(lineSeriesList) != 0 {
|
|
||||||
handler.Add(func() error {
|
|
||||||
_, err := NewLineChart(p, LineChartOption{
|
|
||||||
Theme: opt.theme,
|
|
||||||
Font: opt.font,
|
|
||||||
XAxis: opt.XAxis,
|
|
||||||
}).render(renderResult, lineSeriesList)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// bar chart
|
// bar chart
|
||||||
if len(barSeriesList) != 0 {
|
if len(barSeriesList) != 0 {
|
||||||
handler.Add(func() error {
|
handler.Add(func() error {
|
||||||
|
|
@ -296,11 +344,65 @@ func Render(opt ChartOption) (*Painter, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pie chart
|
||||||
|
if len(pieSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewPieChart(p, PieChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
}).render(renderResult, pieSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// line chart
|
||||||
|
if len(lineSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewLineChart(p, LineChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
}).render(renderResult, lineSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// radar chart
|
||||||
|
if len(radarSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewRadarChart(p, RadarChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
// 相应值
|
||||||
|
RadarIndicators: opt.RadarIndicators,
|
||||||
|
}).render(renderResult, radarSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// funnel chart
|
||||||
|
if len(funnelSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewFunnelChart(p, FunnelChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
}).render(renderResult, funnelSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.Do()
|
err = handler.Do()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for _, item := range opt.Children {
|
||||||
|
item.Parent = p
|
||||||
|
_, err = Render(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
505
echarts.go
Normal file
505
echarts.go
Normal file
|
|
@ -0,0 +1,505 @@
|
||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertToArray(data []byte) []byte {
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if data[0] != '[' {
|
||||||
|
data = []byte("[" + string(data) + "]")
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsPosition string
|
||||||
|
|
||||||
|
func (p *EChartsPosition) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if regexp.MustCompile(`^\d+`).Match(data) {
|
||||||
|
data = []byte(fmt.Sprintf(`"%s"`, string(data)))
|
||||||
|
}
|
||||||
|
s := (*string)(p)
|
||||||
|
return json.Unmarshal(data, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartStyle struct {
|
||||||
|
Color string `json:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EChartStyle) ToStyle() chart.Style {
|
||||||
|
color := parseColor(es.Color)
|
||||||
|
return chart.Style{
|
||||||
|
FillColor: color,
|
||||||
|
FontColor: color,
|
||||||
|
StrokeColor: color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 EChartsSeriesDataValue `json:"value"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ItemStyle EChartStyle `json:"itemStyle"`
|
||||||
|
}
|
||||||
|
type _EChartsSeriesData EChartsSeriesData
|
||||||
|
|
||||||
|
var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`)
|
||||||
|
|
||||||
|
func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if numericRep.Match(data) {
|
||||||
|
v, err := strconv.ParseFloat(string(data), 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
es.Value = EChartsSeriesDataValue{
|
||||||
|
values: []float64{
|
||||||
|
v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := _EChartsSeriesData{}
|
||||||
|
err := json.Unmarshal(data, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
es.Name = v.Name
|
||||||
|
es.Value = v.Value
|
||||||
|
es.ItemStyle = v.ItemStyle
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsXAxisData struct {
|
||||||
|
BoundaryGap *bool `json:"boundaryGap"`
|
||||||
|
SplitNumber int `json:"splitNumber"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
}
|
||||||
|
type EChartsXAxis struct {
|
||||||
|
Data []EChartsXAxisData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
|
||||||
|
data = convertToArray(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &ex.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsAxisLabel struct {
|
||||||
|
Formatter string `json:"formatter"`
|
||||||
|
}
|
||||||
|
type EChartsYAxisData struct {
|
||||||
|
Min *float64 `json:"min"`
|
||||||
|
Max *float64 `json:"max"`
|
||||||
|
AxisLabel EChartsAxisLabel `json:"axisLabel"`
|
||||||
|
AxisLine struct {
|
||||||
|
LineStyle struct {
|
||||||
|
Color string `json:"color"`
|
||||||
|
} `json:"lineStyle"`
|
||||||
|
} `json:"axisLine"`
|
||||||
|
}
|
||||||
|
type EChartsYAxis struct {
|
||||||
|
Data []EChartsYAxisData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
|
||||||
|
data = convertToArray(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &ey.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsPadding struct {
|
||||||
|
Box chart.Box
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EChartsPadding) UnmarshalJSON(data []byte) error {
|
||||||
|
data = convertToArray(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
arr := make([]int, 0)
|
||||||
|
err := json.Unmarshal(data, &arr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch len(arr) {
|
||||||
|
case 1:
|
||||||
|
eb.Box = chart.Box{
|
||||||
|
Left: arr[0],
|
||||||
|
Top: arr[0],
|
||||||
|
Bottom: arr[0],
|
||||||
|
Right: arr[0],
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
eb.Box = chart.Box{
|
||||||
|
Top: arr[0],
|
||||||
|
Bottom: arr[0],
|
||||||
|
Left: arr[1],
|
||||||
|
Right: arr[1],
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result := make([]int, 4)
|
||||||
|
copy(result, arr)
|
||||||
|
if len(arr) == 3 {
|
||||||
|
result[3] = result[1]
|
||||||
|
}
|
||||||
|
// 上右下左
|
||||||
|
eb.Box = chart.Box{
|
||||||
|
Top: result[0],
|
||||||
|
Right: result[1],
|
||||||
|
Bottom: result[2],
|
||||||
|
Left: result[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsLabelOption struct {
|
||||||
|
Show bool `json:"show"`
|
||||||
|
Distance int `json:"distance"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
}
|
||||||
|
type EChartsLegend struct {
|
||||||
|
Show *bool `json:"show"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
Align string `json:"align"`
|
||||||
|
Orient string `json:"orient"`
|
||||||
|
Padding EChartsPadding `json:"padding"`
|
||||||
|
Left EChartsPosition `json:"left"`
|
||||||
|
Top EChartsPosition `json:"top"`
|
||||||
|
TextStyle EChartsTextStyle `json:"textStyle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsMarkData struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
type _EChartsMarkData EChartsMarkData
|
||||||
|
|
||||||
|
func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error {
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data = convertToArray(data)
|
||||||
|
ds := make([]*_EChartsMarkData, 0)
|
||||||
|
err := json.Unmarshal(data, &ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, d := range ds {
|
||||||
|
if d.Type != "" {
|
||||||
|
emd.Type = d.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsMarkPoint struct {
|
||||||
|
SymbolSize int `json:"symbolSize"`
|
||||||
|
Data []EChartsMarkData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint {
|
||||||
|
sp := SeriesMarkPoint{
|
||||||
|
SymbolSize: emp.SymbolSize,
|
||||||
|
}
|
||||||
|
if len(emp.Data) == 0 {
|
||||||
|
return sp
|
||||||
|
}
|
||||||
|
data := make([]SeriesMarkData, len(emp.Data))
|
||||||
|
for index, item := range emp.Data {
|
||||||
|
data[index].Type = item.Type
|
||||||
|
}
|
||||||
|
sp.Data = data
|
||||||
|
return sp
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsMarkLine struct {
|
||||||
|
Data []EChartsMarkData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine {
|
||||||
|
sl := SeriesMarkLine{}
|
||||||
|
if len(eml.Data) == 0 {
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
data := make([]SeriesMarkData, len(eml.Data))
|
||||||
|
for index, item := range eml.Data {
|
||||||
|
data[index].Type = item.Type
|
||||||
|
}
|
||||||
|
sl.Data = data
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsSeries struct {
|
||||||
|
Data []EChartsSeriesData `json:"data"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Radius string `json:"radius"`
|
||||||
|
YAxisIndex int `json:"yAxisIndex"`
|
||||||
|
ItemStyle EChartStyle `json:"itemStyle"`
|
||||||
|
// label的配置
|
||||||
|
Label EChartsLabelOption `json:"label"`
|
||||||
|
MarkPoint EChartsMarkPoint `json:"markPoint"`
|
||||||
|
MarkLine EChartsMarkLine `json:"markLine"`
|
||||||
|
Max *float64 `json:"max"`
|
||||||
|
Min *float64 `json:"min"`
|
||||||
|
}
|
||||||
|
type EChartsSeriesList []EChartsSeries
|
||||||
|
|
||||||
|
func (esList EChartsSeriesList) ToSeriesList() SeriesList {
|
||||||
|
seriesList := make(SeriesList, 0, len(esList))
|
||||||
|
for _, item := range esList {
|
||||||
|
// 如果是pie,则每个子荐生成一个series
|
||||||
|
if item.Type == ChartTypePie {
|
||||||
|
for _, dataItem := range item.Data {
|
||||||
|
seriesList = append(seriesList, Series{
|
||||||
|
Type: item.Type,
|
||||||
|
Name: dataItem.Name,
|
||||||
|
Label: SeriesLabel{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Radius: item.Radius,
|
||||||
|
Data: []SeriesData{
|
||||||
|
{
|
||||||
|
Value: dataItem.Value.First(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 如果是radar或funnel
|
||||||
|
if item.Type == ChartTypeRadar ||
|
||||||
|
item.Type == ChartTypeFunnel {
|
||||||
|
for _, dataItem := range item.Data {
|
||||||
|
seriesList = append(seriesList, Series{
|
||||||
|
Name: dataItem.Name,
|
||||||
|
Type: item.Type,
|
||||||
|
Data: NewSeriesDataFromValues(dataItem.Value.values),
|
||||||
|
Max: item.Max,
|
||||||
|
Min: item.Min,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := make([]SeriesData, len(item.Data))
|
||||||
|
for j, dataItem := range item.Data {
|
||||||
|
data[j] = SeriesData{
|
||||||
|
Value: dataItem.Value.First(),
|
||||||
|
Style: dataItem.ItemStyle.ToStyle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seriesList = append(seriesList, Series{
|
||||||
|
Type: item.Type,
|
||||||
|
Data: data,
|
||||||
|
AxisIndex: item.YAxisIndex,
|
||||||
|
Style: item.ItemStyle.ToStyle(),
|
||||||
|
Label: SeriesLabel{
|
||||||
|
Color: parseColor(item.Label.Color),
|
||||||
|
Show: item.Label.Show,
|
||||||
|
Distance: item.Label.Distance,
|
||||||
|
},
|
||||||
|
Name: item.Name,
|
||||||
|
MarkPoint: item.MarkPoint.ToSeriesMarkPoint(),
|
||||||
|
MarkLine: item.MarkLine.ToSeriesMarkLine(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return seriesList
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsTextStyle struct {
|
||||||
|
Color string `json:"color"`
|
||||||
|
FontFamily string `json:"fontFamily"`
|
||||||
|
FontSize float64 `json:"fontSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et *EChartsTextStyle) ToStyle() chart.Style {
|
||||||
|
s := chart.Style{
|
||||||
|
FontSize: et.FontSize,
|
||||||
|
FontColor: parseColor(et.Color),
|
||||||
|
}
|
||||||
|
if et.FontFamily != "" {
|
||||||
|
s.Font, _ = GetFont(et.FontFamily)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type EChartsOption struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Theme string `json:"theme"`
|
||||||
|
FontFamily string `json:"fontFamily"`
|
||||||
|
Padding EChartsPadding `json:"padding"`
|
||||||
|
Box chart.Box `json:"box"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Title struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Subtext string `json:"subtext"`
|
||||||
|
Left EChartsPosition `json:"left"`
|
||||||
|
Top EChartsPosition `json:"top"`
|
||||||
|
TextStyle EChartsTextStyle `json:"textStyle"`
|
||||||
|
SubtextStyle EChartsTextStyle `json:"subtextStyle"`
|
||||||
|
} `json:"title"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eo *EChartsOption) ToOption() ChartOption {
|
||||||
|
fontFamily := eo.FontFamily
|
||||||
|
if len(fontFamily) == 0 {
|
||||||
|
fontFamily = eo.Title.TextStyle.FontFamily
|
||||||
|
}
|
||||||
|
titleTextStyle := eo.Title.TextStyle.ToStyle()
|
||||||
|
titleSubtextStyle := eo.Title.SubtextStyle.ToStyle()
|
||||||
|
legendTextStyle := eo.Legend.TextStyle.ToStyle()
|
||||||
|
o := ChartOption{
|
||||||
|
Type: eo.Type,
|
||||||
|
FontFamily: fontFamily,
|
||||||
|
Theme: eo.Theme,
|
||||||
|
Title: TitleOption{
|
||||||
|
Text: eo.Title.Text,
|
||||||
|
Subtext: eo.Title.Subtext,
|
||||||
|
FontColor: titleTextStyle.FontColor,
|
||||||
|
FontSize: titleTextStyle.FontSize,
|
||||||
|
SubtextFontSize: titleSubtextStyle.FontSize,
|
||||||
|
SubtextFontColor: titleSubtextStyle.FontColor,
|
||||||
|
Left: string(eo.Title.Left),
|
||||||
|
Top: string(eo.Title.Top),
|
||||||
|
},
|
||||||
|
Legend: LegendOption{
|
||||||
|
Show: eo.Legend.Show,
|
||||||
|
FontSize: legendTextStyle.FontSize,
|
||||||
|
FontColor: legendTextStyle.FontColor,
|
||||||
|
Data: eo.Legend.Data,
|
||||||
|
Left: string(eo.Legend.Left),
|
||||||
|
Top: string(eo.Legend.Top),
|
||||||
|
Align: eo.Legend.Align,
|
||||||
|
Orient: eo.Legend.Orient,
|
||||||
|
},
|
||||||
|
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]
|
||||||
|
o.XAxis = XAxisOption{
|
||||||
|
BoundaryGap: xAxisData.BoundaryGap,
|
||||||
|
Data: xAxisData.Data,
|
||||||
|
SplitNumber: xAxisData.SplitNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data))
|
||||||
|
for index, item := range eo.YAxis.Data {
|
||||||
|
yAxisOptions[index] = YAxisOption{
|
||||||
|
Min: item.Min,
|
||||||
|
Max: item.Max,
|
||||||
|
Formatter: item.AxisLabel.Formatter,
|
||||||
|
Color: parseColor(item.AxisLine.LineStyle.Color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.YAxisOptions = yAxisOptions
|
||||||
|
|
||||||
|
if len(eo.Children) != 0 {
|
||||||
|
o.Children = make([]ChartOption, len(eo.Children))
|
||||||
|
for index, item := range eo.Children {
|
||||||
|
o.Children[index] = item.ToOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderEcharts(options, outputType string) ([]byte, error) {
|
||||||
|
o := EChartsOption{}
|
||||||
|
err := json.Unmarshal([]byte(options), &o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opt := o.ToOption()
|
||||||
|
opt.Type = outputType
|
||||||
|
d, err := Render(opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderEChartsToPNG(options string) ([]byte, error) {
|
||||||
|
return renderEcharts(options, "png")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderEChartsToSVG(options string) ([]byte, error) {
|
||||||
|
return renderEcharts(options, "svg")
|
||||||
|
}
|
||||||
|
|
@ -83,13 +83,13 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
|
||||||
}
|
}
|
||||||
bytesList = append(bytesList, buf)
|
bytesList = append(bytesList, buf)
|
||||||
}
|
}
|
||||||
// for _, opt := range echartsOptions {
|
for _, opt := range echartsOptions {
|
||||||
// buf, err := charts.RenderEChartsToSVG(opt)
|
buf, err := charts.RenderEChartsToSVG(opt)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// panic(err)
|
panic(err)
|
||||||
// }
|
}
|
||||||
// bytesList = append(bytesList, buf)
|
bytesList = append(bytesList, buf)
|
||||||
// }
|
}
|
||||||
|
|
||||||
data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte("")))
|
data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte("")))
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
|
@ -333,6 +333,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 柱状图+标记
|
||||||
{
|
{
|
||||||
Title: charts.TitleOption{
|
Title: charts.TitleOption{
|
||||||
Text: "Rainfall vs Evaporation",
|
Text: "Rainfall vs Evaporation",
|
||||||
|
|
@ -413,6 +414,342 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 双Y轴示例
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Temperature",
|
||||||
|
},
|
||||||
|
XAxis: charts.NewXAxisOption([]string{
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
}),
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Evaporation",
|
||||||
|
"Precipitation",
|
||||||
|
"Temperature",
|
||||||
|
}),
|
||||||
|
YAxisOptions: []charts.YAxisOption{
|
||||||
|
{
|
||||||
|
Formatter: "{value}ml",
|
||||||
|
Color: charts.Color{
|
||||||
|
R: 84,
|
||||||
|
G: 112,
|
||||||
|
B: 198,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Formatter: "{value}°C",
|
||||||
|
Color: charts.Color{
|
||||||
|
R: 250,
|
||||||
|
G: 200,
|
||||||
|
B: 88,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SeriesList: []charts.Series{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeBar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
2.0,
|
||||||
|
4.9,
|
||||||
|
7.0,
|
||||||
|
23.2,
|
||||||
|
25.6,
|
||||||
|
76.7,
|
||||||
|
135.6,
|
||||||
|
162.2,
|
||||||
|
32.6,
|
||||||
|
20.0,
|
||||||
|
6.4,
|
||||||
|
3.3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeBar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
2.6,
|
||||||
|
5.9,
|
||||||
|
9.0,
|
||||||
|
26.4,
|
||||||
|
28.7,
|
||||||
|
70.7,
|
||||||
|
175.6,
|
||||||
|
182.2,
|
||||||
|
48.7,
|
||||||
|
18.8,
|
||||||
|
6.0,
|
||||||
|
2.3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
2.0,
|
||||||
|
2.2,
|
||||||
|
3.3,
|
||||||
|
4.5,
|
||||||
|
6.3,
|
||||||
|
10.2,
|
||||||
|
20.3,
|
||||||
|
23.4,
|
||||||
|
23.0,
|
||||||
|
16.5,
|
||||||
|
12.0,
|
||||||
|
6.2,
|
||||||
|
}),
|
||||||
|
AxisIndex: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 饼图
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Referer of a Website",
|
||||||
|
Subtext: "Fake Data",
|
||||||
|
Left: charts.PositionCenter,
|
||||||
|
},
|
||||||
|
Legend: charts.LegendOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Data: []string{
|
||||||
|
"Search Engine",
|
||||||
|
"Direct",
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
},
|
||||||
|
Left: charts.PositionLeft,
|
||||||
|
},
|
||||||
|
SeriesList: charts.NewPieSeriesList([]float64{
|
||||||
|
1048,
|
||||||
|
735,
|
||||||
|
580,
|
||||||
|
484,
|
||||||
|
300,
|
||||||
|
}, charts.PieSeriesOption{
|
||||||
|
Label: charts.SeriesLabel{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Radius: "35%",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// 雷达图
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Basic Radar Chart",
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
RadarIndicators: []charts.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: charts.SeriesList{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
5000,
|
||||||
|
14000,
|
||||||
|
28000,
|
||||||
|
26000,
|
||||||
|
42000,
|
||||||
|
21000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 漏斗图
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Funnel",
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Show",
|
||||||
|
"Click",
|
||||||
|
"Visit",
|
||||||
|
"Inquiry",
|
||||||
|
"Order",
|
||||||
|
}),
|
||||||
|
SeriesList: []charts.Series{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Show",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Click",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
80,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Visit",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
60,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Inquiry",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
40,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Order",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
20,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 多图展示
|
||||||
|
{
|
||||||
|
Legend: charts.LegendOption{
|
||||||
|
Top: "-90",
|
||||||
|
Data: []string{
|
||||||
|
"Milk Tea",
|
||||||
|
"Matcha Latte",
|
||||||
|
"Cheese Cocoa",
|
||||||
|
"Walnut Brownie",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Padding: charts.Box{
|
||||||
|
Top: 100,
|
||||||
|
Right: 10,
|
||||||
|
Bottom: 10,
|
||||||
|
Left: 10,
|
||||||
|
},
|
||||||
|
XAxis: charts.NewXAxisOption([]string{
|
||||||
|
"2012",
|
||||||
|
"2013",
|
||||||
|
"2014",
|
||||||
|
"2015",
|
||||||
|
"2016",
|
||||||
|
"2017",
|
||||||
|
}),
|
||||||
|
YAxisOptions: []charts.YAxisOption{
|
||||||
|
{
|
||||||
|
|
||||||
|
Min: charts.NewFloatPoint(0),
|
||||||
|
Max: charts.NewFloatPoint(90),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SeriesList: []charts.Series{
|
||||||
|
charts.NewSeriesFromValues([]float64{
|
||||||
|
56.5,
|
||||||
|
82.1,
|
||||||
|
88.7,
|
||||||
|
70.1,
|
||||||
|
53.4,
|
||||||
|
85.1,
|
||||||
|
}),
|
||||||
|
charts.NewSeriesFromValues([]float64{
|
||||||
|
51.1,
|
||||||
|
51.4,
|
||||||
|
55.1,
|
||||||
|
53.3,
|
||||||
|
73.8,
|
||||||
|
68.7,
|
||||||
|
}),
|
||||||
|
charts.NewSeriesFromValues([]float64{
|
||||||
|
40.1,
|
||||||
|
62.2,
|
||||||
|
69.5,
|
||||||
|
36.4,
|
||||||
|
45.2,
|
||||||
|
32.5,
|
||||||
|
}, charts.ChartTypeBar),
|
||||||
|
charts.NewSeriesFromValues([]float64{
|
||||||
|
25.2,
|
||||||
|
37.1,
|
||||||
|
41.2,
|
||||||
|
18,
|
||||||
|
33.9,
|
||||||
|
49.1,
|
||||||
|
}, charts.ChartTypeBar),
|
||||||
|
},
|
||||||
|
Children: []charts.ChartOption{
|
||||||
|
{
|
||||||
|
Legend: charts.LegendOption{
|
||||||
|
Show: charts.FalseFlag(),
|
||||||
|
Data: []string{
|
||||||
|
"Milk Tea",
|
||||||
|
"Matcha Latte",
|
||||||
|
"Cheese Cocoa",
|
||||||
|
"Walnut Brownie",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Box: charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Left: 400,
|
||||||
|
Right: 500,
|
||||||
|
Bottom: 120,
|
||||||
|
},
|
||||||
|
SeriesList: charts.NewPieSeriesList([]float64{
|
||||||
|
435.9,
|
||||||
|
354.3,
|
||||||
|
285.9,
|
||||||
|
204.5,
|
||||||
|
}, charts.PieSeriesOption{
|
||||||
|
Label: charts.SeriesLabel{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Radius: "35%",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
handler(w, req, chartOptions, nil)
|
handler(w, req, chartOptions, nil)
|
||||||
}
|
}
|
||||||
|
|
@ -879,12 +1216,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
23.2,
|
23.2,
|
||||||
25.6,
|
25.6,
|
||||||
76.7,
|
76.7,
|
||||||
135.6,
|
135.6
|
||||||
162.2,
|
|
||||||
32.6,
|
|
||||||
20,
|
|
||||||
6.4,
|
|
||||||
3.3
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -898,12 +1230,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
26.4,
|
26.4,
|
||||||
28.7,
|
28.7,
|
||||||
70.7,
|
70.7,
|
||||||
175.6,
|
175.6
|
||||||
182.2,
|
|
||||||
48.7,
|
|
||||||
18.8,
|
|
||||||
6,
|
|
||||||
2.3
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -918,12 +1245,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
4.5,
|
4.5,
|
||||||
6.3,
|
6.3,
|
||||||
10.2,
|
10.2,
|
||||||
20.3,
|
20.3
|
||||||
23.4,
|
|
||||||
23,
|
|
||||||
16.5,
|
|
||||||
12,
|
|
||||||
6.2
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
97
examples/funnel_chart/main.go
Normal file
97
examples/funnel_chart/main.go
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "funnel-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p, err := charts.NewPainter(charts.PainterOptions{
|
||||||
|
Width: 800,
|
||||||
|
Height: 600,
|
||||||
|
Type: charts.ChartOutputPNG,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = charts.NewFunnelChart(p, charts.FunnelChartOption{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Funnel",
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Show",
|
||||||
|
"Click",
|
||||||
|
"Visit",
|
||||||
|
"Inquiry",
|
||||||
|
"Order",
|
||||||
|
}),
|
||||||
|
SeriesList: []charts.Series{
|
||||||
|
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Show",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Click",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
80,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Visit",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
60,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Inquiry",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
40,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Order",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
20,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).Render()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
83
examples/pie_chart/main.go
Normal file
83
examples/pie_chart/main.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "pie-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p, err := charts.NewPainter(charts.PainterOptions{
|
||||||
|
Width: 800,
|
||||||
|
Height: 600,
|
||||||
|
Type: charts.ChartOutputPNG,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = charts.NewPieChart(p, charts.PieChartOption{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Rainfall vs Evaporation",
|
||||||
|
Subtext: "Fake Data",
|
||||||
|
Left: charts.PositionCenter,
|
||||||
|
},
|
||||||
|
Padding: charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
},
|
||||||
|
Legend: charts.LegendOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Data: []string{
|
||||||
|
"Search Engine",
|
||||||
|
"Direct",
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
},
|
||||||
|
Left: charts.PositionLeft,
|
||||||
|
},
|
||||||
|
SeriesList: charts.NewPieSeriesList([]float64{
|
||||||
|
1048,
|
||||||
|
735,
|
||||||
|
580,
|
||||||
|
484,
|
||||||
|
300,
|
||||||
|
}, charts.PieSeriesOption{
|
||||||
|
Label: charts.SeriesLabel{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Radius: "35%",
|
||||||
|
}),
|
||||||
|
}).Render()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
112
examples/radar_chart/main.go
Normal file
112
examples/radar_chart/main.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "radar-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p, err := charts.NewPainter(charts.PainterOptions{
|
||||||
|
Width: 800,
|
||||||
|
Height: 600,
|
||||||
|
Type: charts.ChartOutputPNG,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = charts.NewRadarChart(p, charts.RadarChartOption{
|
||||||
|
Padding: charts.Box{
|
||||||
|
Left: 10,
|
||||||
|
Top: 10,
|
||||||
|
Right: 10,
|
||||||
|
Bottom: 10,
|
||||||
|
},
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Basic Radar Chart",
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
RadarIndicators: []charts.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: charts.SeriesList{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
5000,
|
||||||
|
14000,
|
||||||
|
28000,
|
||||||
|
26000,
|
||||||
|
42000,
|
||||||
|
21000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).Render()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
172
funnel_chart.go
Normal file
172
funnel_chart.go
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type funnelChart struct {
|
||||||
|
p *Painter
|
||||||
|
opt *FunnelChartOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
|
||||||
|
if opt.Theme == nil {
|
||||||
|
opt.Theme = NewTheme("")
|
||||||
|
}
|
||||||
|
return &funnelChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunnelChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := f.opt
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
max := seriesList[0].Data[0].Value
|
||||||
|
min := float64(0)
|
||||||
|
for _, item := range seriesList {
|
||||||
|
if item.Max != nil {
|
||||||
|
max = *item.Max
|
||||||
|
}
|
||||||
|
if item.Min != nil {
|
||||||
|
min = *item.Min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme := opt.Theme
|
||||||
|
gap := 2
|
||||||
|
height := seriesPainter.Height()
|
||||||
|
width := seriesPainter.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
|
||||||
|
widthPercent := (value - min) / (max - min)
|
||||||
|
w := int(widthPercent * float64(width))
|
||||||
|
widthList[index] = w
|
||||||
|
p := humanize.CommafWithDigits(value/max*100, 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)
|
||||||
|
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: color,
|
||||||
|
}).FillArea(points)
|
||||||
|
|
||||||
|
// 文本
|
||||||
|
text := textList[index]
|
||||||
|
seriesPainter.OverrideTextStyle(Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
Font: opt.Font,
|
||||||
|
})
|
||||||
|
textBox := seriesPainter.MeasureText(text)
|
||||||
|
textX := width>>1 - textBox.Width()>>1
|
||||||
|
textY := y + h>>1
|
||||||
|
seriesPainter.Text(text, textX, textY)
|
||||||
|
y += (h + gap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *funnelChart) Render() (Box, error) {
|
||||||
|
p := f.p
|
||||||
|
opt := f.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeFunnel)
|
||||||
|
return f.render(renderResult, seriesList)
|
||||||
|
}
|
||||||
|
|
@ -92,13 +92,9 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
|
||||||
Size: seriesPainter.Width(),
|
Size: seriesPainter.Width(),
|
||||||
})
|
})
|
||||||
|
|
||||||
for i := range seriesList {
|
for index := range seriesList {
|
||||||
series := seriesList[i]
|
series := seriesList[index]
|
||||||
index := series.index
|
seriesColor := theme.GetSeriesColor(series.index)
|
||||||
if index == 0 {
|
|
||||||
index = i
|
|
||||||
}
|
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
|
||||||
divideValues := yRange.AutoDivide()
|
divideValues := yRange.AutoDivide()
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
if j >= yRange.divideCount {
|
if j >= yRange.divideCount {
|
||||||
|
|
@ -108,8 +104,8 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
|
||||||
j = yRange.divideCount - j - 1
|
j = yRange.divideCount - j - 1
|
||||||
y := divideValues[j]
|
y := divideValues[j]
|
||||||
y += margin
|
y += margin
|
||||||
if i != 0 {
|
if index != 0 {
|
||||||
y += i * (barHeight + barMargin)
|
y += index * (barHeight + barMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := int(xRange.getHeight(item.Value))
|
w := int(xRange.getHeight(item.Value))
|
||||||
|
|
|
||||||
21
legend.go
21
legend.go
|
|
@ -56,6 +56,8 @@ type LegendOption struct {
|
||||||
FontSize float64
|
FontSize float64
|
||||||
// FontColor color of legend text
|
// FontColor color of legend text
|
||||||
FontColor Color
|
FontColor Color
|
||||||
|
// The flag for show legend, set this to *false will hide legend
|
||||||
|
Show *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLegendOption(labels []string, left ...string) LegendOption {
|
func NewLegendOption(labels []string, left ...string) LegendOption {
|
||||||
|
|
@ -68,6 +70,17 @@ func NewLegendOption(labels []string, left ...string) LegendOption {
|
||||||
return opt
|
return opt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opt *LegendOption) IsEmpty() bool {
|
||||||
|
isEmpty := true
|
||||||
|
for _, v := range opt.Data {
|
||||||
|
if v != "" {
|
||||||
|
isEmpty = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
||||||
return &legendPainter{
|
return &legendPainter{
|
||||||
p: p,
|
p: p,
|
||||||
|
|
@ -78,6 +91,10 @@ func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
||||||
func (l *legendPainter) Render() (Box, error) {
|
func (l *legendPainter) Render() (Box, error) {
|
||||||
opt := l.opt
|
opt := l.opt
|
||||||
theme := opt.Theme
|
theme := opt.Theme
|
||||||
|
if opt.IsEmpty() ||
|
||||||
|
(opt.Show != nil && !*opt.Show) {
|
||||||
|
return BoxZero, nil
|
||||||
|
}
|
||||||
if theme == nil {
|
if theme == nil {
|
||||||
theme = l.p.theme
|
theme = l.p.theme
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +107,9 @@ func (l *legendPainter) Render() (Box, error) {
|
||||||
if opt.Left == "" {
|
if opt.Left == "" {
|
||||||
opt.Left = PositionCenter
|
opt.Left = PositionCenter
|
||||||
}
|
}
|
||||||
p := l.p
|
p := l.p.Child(PainterPaddingOption(Box{
|
||||||
|
Top: 5,
|
||||||
|
}))
|
||||||
p.SetTextStyle(Style{
|
p.SetTextStyle(Style{
|
||||||
FontSize: opt.FontSize,
|
FontSize: opt.FontSize,
|
||||||
FontColor: opt.FontColor,
|
FontColor: opt.FontColor,
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
|
||||||
}
|
}
|
||||||
for index := range seriesList {
|
for index := range seriesList {
|
||||||
series := seriesList[index]
|
series := seriesList[index]
|
||||||
seriesColor := opt.Theme.GetSeriesColor(index)
|
seriesColor := opt.Theme.GetSeriesColor(series.index)
|
||||||
drawingStyle := Style{
|
drawingStyle := Style{
|
||||||
StrokeColor: seriesColor,
|
StrokeColor: seriesColor,
|
||||||
StrokeWidth: defaultStrokeWidth,
|
StrokeWidth: defaultStrokeWidth,
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
||||||
values = autoDivide(width, count)
|
values = autoDivide(width, count)
|
||||||
}
|
}
|
||||||
for index, text := range opt.TextList {
|
for index, text := range opt.TextList {
|
||||||
|
if index%opt.Unit != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
box := p.MeasureText(text)
|
box := p.MeasureText(text)
|
||||||
start := values[index]
|
start := values[index]
|
||||||
if positionCenter {
|
if positionCenter {
|
||||||
|
|
|
||||||
211
pie_chart.go
Normal file
211
pie_chart.go
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pieChart struct {
|
||||||
|
p *Painter
|
||||||
|
opt *PieChartOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type PieChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
|
||||||
|
if opt.Theme == nil {
|
||||||
|
opt.Theme = NewTheme("")
|
||||||
|
}
|
||||||
|
return &pieChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := p.opt
|
||||||
|
values := make([]float64, len(seriesList))
|
||||||
|
total := float64(0)
|
||||||
|
radiusValue := ""
|
||||||
|
for index, series := range seriesList {
|
||||||
|
if len(series.Radius) != 0 {
|
||||||
|
radiusValue = series.Radius
|
||||||
|
}
|
||||||
|
value := float64(0)
|
||||||
|
for _, item := range series.Data {
|
||||||
|
value += item.Value
|
||||||
|
}
|
||||||
|
values[index] = value
|
||||||
|
total += value
|
||||||
|
}
|
||||||
|
if total <= 0 {
|
||||||
|
return BoxZero, errors.New("The sum value of pie chart should gt 0")
|
||||||
|
}
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
cx := seriesPainter.Width() >> 1
|
||||||
|
cy := seriesPainter.Height() >> 1
|
||||||
|
|
||||||
|
diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height())
|
||||||
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
|
labelLineWidth := 15
|
||||||
|
if radius < 50 {
|
||||||
|
labelLineWidth = 10
|
||||||
|
}
|
||||||
|
labelRadius := radius + float64(labelLineWidth)
|
||||||
|
seriesNames := opt.Legend.Data
|
||||||
|
if len(seriesNames) == 0 {
|
||||||
|
seriesNames = seriesList.Names()
|
||||||
|
}
|
||||||
|
theme := opt.Theme
|
||||||
|
if len(values) == 1 {
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeWidth: 1,
|
||||||
|
StrokeColor: theme.GetSeriesColor(0),
|
||||||
|
FillColor: theme.GetSeriesColor(0),
|
||||||
|
})
|
||||||
|
seriesPainter.MoveTo(cx, cy).
|
||||||
|
Circle(radius, cx, cy)
|
||||||
|
} else {
|
||||||
|
currentValue := float64(0)
|
||||||
|
prevEndX := 0
|
||||||
|
prevEndY := 0
|
||||||
|
for index, v := range values {
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeWidth: 1,
|
||||||
|
StrokeColor: theme.GetSeriesColor(index),
|
||||||
|
FillColor: theme.GetSeriesColor(index),
|
||||||
|
})
|
||||||
|
seriesPainter.MoveTo(cx, cy)
|
||||||
|
start := chart.PercentToRadians(currentValue/total) - math.Pi/2
|
||||||
|
currentValue += v
|
||||||
|
percent := (v / total)
|
||||||
|
delta := chart.PercentToRadians(percent)
|
||||||
|
seriesPainter.ArcTo(cx, cy, radius, radius, start, delta).
|
||||||
|
LineTo(cx, cy).
|
||||||
|
Close().
|
||||||
|
FillStroke()
|
||||||
|
|
||||||
|
series := seriesList[index]
|
||||||
|
// 是否显示label
|
||||||
|
showLabel := series.Label.Show
|
||||||
|
if !showLabel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// label的角度为饼块中间
|
||||||
|
angle := start + delta/2
|
||||||
|
startx := cx + int(radius*math.Cos(angle))
|
||||||
|
starty := cy + int(radius*math.Sin(angle))
|
||||||
|
|
||||||
|
endx := cx + int(labelRadius*math.Cos(angle))
|
||||||
|
endy := cy + int(labelRadius*math.Sin(angle))
|
||||||
|
// 计算是否有重叠,如果有则调整y坐标位置
|
||||||
|
if index != 0 &&
|
||||||
|
math.Abs(float64(endx-prevEndX)) < labelFontSize &&
|
||||||
|
math.Abs(float64(endy-prevEndY)) < labelFontSize {
|
||||||
|
endy -= (labelFontSize << 1)
|
||||||
|
}
|
||||||
|
prevEndX = endx
|
||||||
|
prevEndY = endy
|
||||||
|
|
||||||
|
seriesPainter.MoveTo(startx, starty)
|
||||||
|
seriesPainter.LineTo(endx, endy)
|
||||||
|
offset := labelLineWidth
|
||||||
|
if endx < cx {
|
||||||
|
offset *= -1
|
||||||
|
}
|
||||||
|
seriesPainter.MoveTo(endx, endy)
|
||||||
|
endx += offset
|
||||||
|
seriesPainter.LineTo(endx, endy)
|
||||||
|
seriesPainter.Stroke()
|
||||||
|
|
||||||
|
textStyle := Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
Font: opt.Font,
|
||||||
|
}
|
||||||
|
if !series.Label.Color.IsZero() {
|
||||||
|
textStyle.FontColor = series.Label.Color
|
||||||
|
}
|
||||||
|
seriesPainter.OverrideTextStyle(textStyle)
|
||||||
|
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
|
||||||
|
textBox := seriesPainter.MeasureText(text)
|
||||||
|
textMargin := 3
|
||||||
|
x := endx + textMargin
|
||||||
|
y := endy + textBox.Height()>>1 - 1
|
||||||
|
if offset < 0 {
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
x = endx - textWidth - textMargin
|
||||||
|
}
|
||||||
|
seriesPainter.Text(text, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pieChart) Render() (Box, error) {
|
||||||
|
opt := p.opt
|
||||||
|
|
||||||
|
renderResult, err := defaultRender(p.p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
backgroundIsFilled: opt.backgroundIsFilled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypePie)
|
||||||
|
return p.render(renderResult, seriesList)
|
||||||
|
}
|
||||||
245
radar_chart.go
Normal file
245
radar_chart.go
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type radarChart struct {
|
||||||
|
p *Painter
|
||||||
|
opt *RadarChartOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadarIndicator struct {
|
||||||
|
// Indicator's name
|
||||||
|
Name string
|
||||||
|
// The maximum value of indicator
|
||||||
|
Max float64
|
||||||
|
// The minimum value of indicator
|
||||||
|
Min float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type RadarChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// The radar indicator list
|
||||||
|
RadarIndicators []RadarIndicator
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart {
|
||||||
|
if opt.Theme == nil {
|
||||||
|
opt.Theme = NewTheme("")
|
||||||
|
}
|
||||||
|
return &radarChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := r.opt
|
||||||
|
indicators := opt.RadarIndicators
|
||||||
|
sides := len(indicators)
|
||||||
|
if sides < 3 {
|
||||||
|
return BoxZero, errors.New("The count of indicator should be >= 3")
|
||||||
|
}
|
||||||
|
maxValues := make([]float64, len(indicators))
|
||||||
|
for _, series := range seriesList {
|
||||||
|
for index, item := range series.Data {
|
||||||
|
if index < len(maxValues) && item.Value > maxValues[index] {
|
||||||
|
maxValues[index] = item.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, indicator := range indicators {
|
||||||
|
if indicator.Max <= 0 {
|
||||||
|
indicators[index].Max = maxValues[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
radiusValue := ""
|
||||||
|
for _, series := range seriesList {
|
||||||
|
if len(series.Radius) != 0 {
|
||||||
|
radiusValue = series.Radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
theme := opt.Theme
|
||||||
|
|
||||||
|
cx := seriesPainter.Width() >> 1
|
||||||
|
cy := seriesPainter.Height() >> 1
|
||||||
|
diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height())
|
||||||
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
|
divideCount := 5
|
||||||
|
divideRadius := float64(int(radius / float64(divideCount)))
|
||||||
|
radius = divideRadius * float64(divideCount)
|
||||||
|
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeColor: theme.GetAxisSplitLineColor(),
|
||||||
|
StrokeWidth: 1,
|
||||||
|
})
|
||||||
|
center := Point{
|
||||||
|
X: cx,
|
||||||
|
Y: cy,
|
||||||
|
}
|
||||||
|
for i := 0; i < divideCount; i++ {
|
||||||
|
seriesPainter.Polygon(center, divideRadius*float64(i+1), sides)
|
||||||
|
}
|
||||||
|
points := getPolygonPoints(center, radius, sides)
|
||||||
|
for _, p := range points {
|
||||||
|
seriesPainter.MoveTo(center.X, center.Y)
|
||||||
|
seriesPainter.LineTo(p.X, p.Y)
|
||||||
|
seriesPainter.Stroke()
|
||||||
|
}
|
||||||
|
seriesPainter.OverrideTextStyle(Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
Font: opt.Font,
|
||||||
|
})
|
||||||
|
offset := 5
|
||||||
|
// 文本生成
|
||||||
|
for index, p := range points {
|
||||||
|
name := indicators[index].Name
|
||||||
|
b := seriesPainter.MeasureText(name)
|
||||||
|
isXCenter := p.X == center.X
|
||||||
|
isYCenter := p.Y == center.Y
|
||||||
|
isRight := p.X > center.X
|
||||||
|
isLeft := p.X < center.X
|
||||||
|
isTop := p.Y < center.Y
|
||||||
|
isBottom := p.Y > center.Y
|
||||||
|
x := p.X
|
||||||
|
y := p.Y
|
||||||
|
if isXCenter {
|
||||||
|
x -= b.Width() >> 1
|
||||||
|
if isTop {
|
||||||
|
y -= b.Height()
|
||||||
|
} else {
|
||||||
|
y += b.Height()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isYCenter {
|
||||||
|
y += b.Height() >> 1
|
||||||
|
}
|
||||||
|
if isTop {
|
||||||
|
y += offset
|
||||||
|
}
|
||||||
|
if isBottom {
|
||||||
|
y += offset
|
||||||
|
}
|
||||||
|
if isRight {
|
||||||
|
x += offset
|
||||||
|
}
|
||||||
|
if isLeft {
|
||||||
|
x -= (b.Width() + offset)
|
||||||
|
}
|
||||||
|
seriesPainter.Text(name, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 雷达图
|
||||||
|
angles := getPolygonPointAngles(sides)
|
||||||
|
maxCount := len(indicators)
|
||||||
|
for _, series := range seriesList {
|
||||||
|
linePoints := make([]Point, 0, maxCount)
|
||||||
|
for j, item := range series.Data {
|
||||||
|
if j >= maxCount {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indicator := 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(series.index)
|
||||||
|
dotFillColor := drawing.ColorWhite
|
||||||
|
if theme.IsDark() {
|
||||||
|
dotFillColor = color
|
||||||
|
}
|
||||||
|
linePoints = append(linePoints, linePoints[0])
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeColor: color,
|
||||||
|
StrokeWidth: defaultStrokeWidth,
|
||||||
|
DotWidth: defaultDotWidth,
|
||||||
|
DotColor: color,
|
||||||
|
FillColor: color.WithAlpha(20),
|
||||||
|
})
|
||||||
|
seriesPainter.LineStroke(linePoints).
|
||||||
|
FillArea(linePoints)
|
||||||
|
dotWith := 2.0
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeWidth: defaultStrokeWidth,
|
||||||
|
StrokeColor: color,
|
||||||
|
FillColor: dotFillColor,
|
||||||
|
})
|
||||||
|
for _, point := range linePoints {
|
||||||
|
seriesPainter.Circle(dotWith, point.X, point.Y)
|
||||||
|
seriesPainter.FillStroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *radarChart) Render() (Box, error) {
|
||||||
|
p := r.p
|
||||||
|
opt := r.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
backgroundIsFilled: opt.backgroundIsFilled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeRadar)
|
||||||
|
return r.render(renderResult, seriesList)
|
||||||
|
}
|
||||||
5
xaxis.go
5
xaxis.go
|
|
@ -38,8 +38,8 @@ type XAxisOption struct {
|
||||||
Theme ColorPalette
|
Theme ColorPalette
|
||||||
// The font size of x axis label
|
// The font size of x axis label
|
||||||
FontSize float64
|
FontSize float64
|
||||||
// Hidden x axis
|
// The flag for show axis, set this to *false will hide axis
|
||||||
Hidden bool
|
Show *bool
|
||||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||||
SplitNumber int
|
SplitNumber int
|
||||||
// The position of axis, it can be 'top' or 'bottom'
|
// The position of axis, it can be 'top' or 'bottom'
|
||||||
|
|
@ -78,6 +78,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
|
||||||
FontSize: opt.FontSize,
|
FontSize: opt.FontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
FontColor: opt.FontColor,
|
FontColor: opt.FontColor,
|
||||||
|
Show: opt.Show,
|
||||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||||
}
|
}
|
||||||
if opt.isValueAxis {
|
if opt.isValueAxis {
|
||||||
|
|
|
||||||
28
yaxis.go
28
yaxis.go
|
|
@ -25,6 +25,10 @@ package charts
|
||||||
import "github.com/golang/freetype/truetype"
|
import "github.com/golang/freetype/truetype"
|
||||||
|
|
||||||
type YAxisOption struct {
|
type YAxisOption struct {
|
||||||
|
// The minimun value of axis.
|
||||||
|
Min *float64
|
||||||
|
// The maximum value of axis.
|
||||||
|
Max *float64
|
||||||
// The font of y axis
|
// The font of y axis
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
// The data value of x axis
|
// The data value of x axis
|
||||||
|
|
@ -36,7 +40,13 @@ type YAxisOption struct {
|
||||||
// The position of axis, it can be 'left' or 'right'
|
// The position of axis, it can be 'left' or 'right'
|
||||||
Position string
|
Position string
|
||||||
// The color of label
|
// The color of label
|
||||||
FontColor Color
|
FontColor Color
|
||||||
|
// Formatter for y axis text value
|
||||||
|
Formatter string
|
||||||
|
// Color for y axis
|
||||||
|
Color Color
|
||||||
|
// The flag for show axis, set this to *false will hide axis
|
||||||
|
Show *bool
|
||||||
isCategoryAxis bool
|
isCategoryAxis bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +70,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
||||||
position = PositionRight
|
position = PositionRight
|
||||||
}
|
}
|
||||||
axisOpt := AxisOption{
|
axisOpt := AxisOption{
|
||||||
|
Formatter: opt.Formatter,
|
||||||
Theme: opt.Theme,
|
Theme: opt.Theme,
|
||||||
Data: opt.Data,
|
Data: opt.Data,
|
||||||
Position: position,
|
Position: position,
|
||||||
|
|
@ -70,6 +81,11 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
||||||
BoundaryGap: FalseFlag(),
|
BoundaryGap: FalseFlag(),
|
||||||
SplitLineShow: true,
|
SplitLineShow: true,
|
||||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||||
|
Show: opt.Show,
|
||||||
|
}
|
||||||
|
if !opt.Color.IsZero() {
|
||||||
|
axisOpt.FontColor = opt.Color
|
||||||
|
axisOpt.StrokeColor = opt.Color
|
||||||
}
|
}
|
||||||
if opt.isCategoryAxis {
|
if opt.isCategoryAxis {
|
||||||
axisOpt.BoundaryGap = TrueFlag()
|
axisOpt.BoundaryGap = TrueFlag()
|
||||||
|
|
@ -85,3 +101,13 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||||
}))
|
}))
|
||||||
return NewAxisPainter(p, opt.ToAxisOption())
|
return NewAxisPainter(p, opt.ToAxisOption())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||||
|
p = p.Child(PainterPaddingOption(Box{
|
||||||
|
Bottom: defaultXAxisHeight,
|
||||||
|
}))
|
||||||
|
axisOpt := opt.ToAxisOption()
|
||||||
|
axisOpt.Position = PositionRight
|
||||||
|
axisOpt.SplitLineShow = false
|
||||||
|
return NewAxisPainter(p, axisOpt)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue