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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
type axisPainter struct {
|
||||
|
|
@ -41,11 +44,15 @@ func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter {
|
|||
type AxisOption struct {
|
||||
// The theme of chart
|
||||
Theme ColorPalette
|
||||
// Formatter for y axis text value
|
||||
Formatter string
|
||||
// The label of axis
|
||||
Data []string
|
||||
// The boundary gap on both sides of a coordinate axis.
|
||||
// Nil or *true means the center part of two axis ticks
|
||||
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'
|
||||
Position string
|
||||
// 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
|
||||
top := a.p
|
||||
theme := opt.Theme
|
||||
if opt.Show != nil && !*opt.Show {
|
||||
return BoxZero, nil
|
||||
}
|
||||
|
||||
strokeWidth := opt.StrokeWidth
|
||||
if strokeWidth == 0 {
|
||||
|
|
@ -97,10 +107,15 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
strokeColor = theme.GetAxisStrokeColor()
|
||||
}
|
||||
|
||||
tickCount := opt.SplitNumber
|
||||
if tickCount == 0 {
|
||||
tickCount = len(opt.Data)
|
||||
data := opt.Data
|
||||
formatter := opt.Formatter
|
||||
if len(formatter) != 0 {
|
||||
for index, text := range data {
|
||||
data[index] = strings.ReplaceAll(formatter, "{value}", text)
|
||||
}
|
||||
}
|
||||
dataCount := len(data)
|
||||
tickCount := dataCount
|
||||
|
||||
boundaryGap := true
|
||||
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
|
||||
|
|
@ -118,8 +133,6 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
labelPosition = PositionCenter
|
||||
}
|
||||
|
||||
// TODO 计算unit
|
||||
unit := 1
|
||||
// 如果小于0,则表示不处理
|
||||
tickLength := getDefaultInt(opt.TickLength, 5)
|
||||
labelMargin := getDefaultInt(opt.LabelMargin, 5)
|
||||
|
|
@ -133,7 +146,9 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
}
|
||||
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
|
||||
height := 0
|
||||
|
|
@ -226,7 +241,7 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
Right: labelPaddingRight,
|
||||
})).MultiText(MultiTextOption{
|
||||
Align: textAlign,
|
||||
TextList: opt.Data,
|
||||
TextList: data,
|
||||
Orient: orient,
|
||||
Unit: unit,
|
||||
Position: labelPosition,
|
||||
|
|
@ -242,10 +257,7 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
x0 = 0
|
||||
x1 = top.Width() - p.Width()
|
||||
}
|
||||
for index, y := range autoDivide(height, tickCount) {
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
for _, y := range autoDivide(height, tickCount) {
|
||||
top.LineStroke([]Point{
|
||||
{
|
||||
X: x0,
|
||||
|
|
|
|||
16
bar_chart.go
16
bar_chart.go
|
|
@ -95,14 +95,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
|||
markPointPainter,
|
||||
markLinePainter,
|
||||
}
|
||||
for i := range seriesList {
|
||||
series := seriesList[i]
|
||||
for index := range seriesList {
|
||||
series := seriesList[index]
|
||||
yRange := result.axisRanges[series.AxisIndex]
|
||||
index := series.index
|
||||
if index == 0 {
|
||||
index = i
|
||||
}
|
||||
seriesColor := theme.GetSeriesColor(index)
|
||||
seriesColor := theme.GetSeriesColor(series.index)
|
||||
|
||||
divideValues := xRange.AutoDivide()
|
||||
points := make([]Point, len(series.Data))
|
||||
|
|
@ -112,8 +108,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
|||
}
|
||||
x := divideValues[j]
|
||||
x += margin
|
||||
if i != 0 {
|
||||
x += i * (barWidth + barMargin)
|
||||
if index != 0 {
|
||||
x += index * (barWidth + barMargin)
|
||||
}
|
||||
|
||||
h := int(yRange.getHeight(item.Value))
|
||||
|
|
@ -151,7 +147,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
|
|||
if distance == 0 {
|
||||
distance = 5
|
||||
}
|
||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1)
|
||||
labelStyle := Style{
|
||||
FontColor: theme.GetTextColor(),
|
||||
FontSize: labelFontSize,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ type ChartOption struct {
|
|||
// The series list
|
||||
SeriesList SeriesList
|
||||
// The radar indicator list
|
||||
// RadarIndicators []RadarIndicator
|
||||
RadarIndicators []RadarIndicator
|
||||
// The background color of chart
|
||||
BackgroundColor Color
|
||||
// The child charts
|
||||
|
|
|
|||
150
charts.go
150
charts.go
|
|
@ -22,7 +22,10 @@
|
|||
|
||||
package charts
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const labelFontSize = 10
|
||||
const defaultDotWidth = 2.0
|
||||
|
|
@ -140,16 +143,29 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
|||
if containsInt(axisIndexList, series.AxisIndex) {
|
||||
continue
|
||||
}
|
||||
axisIndexList = append(axisIndexList, series.index)
|
||||
axisIndexList = append(axisIndexList, series.AxisIndex)
|
||||
}
|
||||
// 高度需要减去x轴的高度
|
||||
rangeHeight := p.Height() - defaultXAxisHeight
|
||||
rangeWidthLeft := 0
|
||||
rangeWidthRight := 0
|
||||
|
||||
// 倒序
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList)))
|
||||
|
||||
// 计算对应的axis range
|
||||
for _, index := range axisIndexList {
|
||||
yAxisOption := YAxisOption{}
|
||||
if len(opt.YAxisOptions) > index {
|
||||
yAxisOption = opt.YAxisOptions[index]
|
||||
}
|
||||
max, min := opt.SeriesList.GetMaxMin(index)
|
||||
if yAxisOption.Min != nil {
|
||||
min = *yAxisOption.Min
|
||||
}
|
||||
if yAxisOption.Max != nil {
|
||||
max = *yAxisOption.Max
|
||||
}
|
||||
r := NewRange(AxisRangeOption{
|
||||
Min: min,
|
||||
Max: max,
|
||||
|
|
@ -159,10 +175,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
|||
DivideCount: defaultAxisDivideCount,
|
||||
})
|
||||
result.axisRanges[index] = r
|
||||
yAxisOption := YAxisOption{}
|
||||
if len(opt.YAxisOptions) > index {
|
||||
yAxisOption = opt.YAxisOptions[index]
|
||||
}
|
||||
|
||||
if yAxisOption.Theme == nil {
|
||||
yAxisOption.Theme = opt.Theme
|
||||
}
|
||||
|
|
@ -175,7 +188,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
|||
}
|
||||
reverseStringSlice(yAxisOption.Data)
|
||||
// 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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -191,7 +213,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
|
|||
opt.XAxis.Theme = opt.Theme
|
||||
}
|
||||
xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
|
||||
Left: rangeWidthLeft,
|
||||
Left: rangeWidthLeft,
|
||||
Right: rangeWidthRight,
|
||||
})), opt.XAxis)
|
||||
_, err := xAxis.Render()
|
||||
if err != nil {
|
||||
|
|
@ -219,7 +242,9 @@ func doRender(renderers ...Renderer) error {
|
|||
func Render(opt ChartOption) (*Painter, error) {
|
||||
opt.fillDefault()
|
||||
|
||||
isChild := true
|
||||
if opt.Parent == nil {
|
||||
isChild = false
|
||||
p, err := NewPainter(PainterOptions{
|
||||
Type: opt.Type,
|
||||
Width: opt.Width,
|
||||
|
|
@ -231,21 +256,40 @@ func Render(opt ChartOption) (*Painter, error) {
|
|||
opt.Parent = p
|
||||
}
|
||||
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.init()
|
||||
|
||||
seriesCount := len(seriesList)
|
||||
|
||||
// line chart
|
||||
lineSeriesList := seriesList.Filter(ChartTypeLine)
|
||||
barSeriesList := seriesList.Filter(ChartTypeBar)
|
||||
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")
|
||||
}
|
||||
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
|
||||
|
||||
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||
renderOpt := defaultRenderOption{
|
||||
Theme: opt.theme,
|
||||
Padding: opt.Padding,
|
||||
SeriesList: opt.SeriesList,
|
||||
|
|
@ -254,24 +298,28 @@ func Render(opt ChartOption) (*Painter, error) {
|
|||
TitleOption: opt.Title,
|
||||
LegendOption: opt.Legend,
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
if len(barSeriesList) != 0 {
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range opt.Children {
|
||||
item.Parent = p
|
||||
_, err = Render(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// for _, opt := range echartsOptions {
|
||||
// buf, err := charts.RenderEChartsToSVG(opt)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// bytesList = append(bytesList, buf)
|
||||
// }
|
||||
for _, opt := range echartsOptions {
|
||||
buf, err := charts.RenderEChartsToSVG(opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bytesList = append(bytesList, buf)
|
||||
}
|
||||
|
||||
data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte("")))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
|
@ -333,6 +333,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
},
|
||||
},
|
||||
// 柱状图+标记
|
||||
{
|
||||
Title: charts.TitleOption{
|
||||
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)
|
||||
}
|
||||
|
|
@ -879,12 +1216,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
|||
23.2,
|
||||
25.6,
|
||||
76.7,
|
||||
135.6,
|
||||
162.2,
|
||||
32.6,
|
||||
20,
|
||||
6.4,
|
||||
3.3
|
||||
135.6
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -898,12 +1230,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
|||
26.4,
|
||||
28.7,
|
||||
70.7,
|
||||
175.6,
|
||||
182.2,
|
||||
48.7,
|
||||
18.8,
|
||||
6,
|
||||
2.3
|
||||
175.6
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -918,12 +1245,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
|||
4.5,
|
||||
6.3,
|
||||
10.2,
|
||||
20.3,
|
||||
23.4,
|
||||
23,
|
||||
16.5,
|
||||
12,
|
||||
6.2
|
||||
20.3
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
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(),
|
||||
})
|
||||
|
||||
for i := range seriesList {
|
||||
series := seriesList[i]
|
||||
index := series.index
|
||||
if index == 0 {
|
||||
index = i
|
||||
}
|
||||
seriesColor := theme.GetSeriesColor(index)
|
||||
for index := range seriesList {
|
||||
series := seriesList[index]
|
||||
seriesColor := theme.GetSeriesColor(series.index)
|
||||
divideValues := yRange.AutoDivide()
|
||||
for j, item := range series.Data {
|
||||
if j >= yRange.divideCount {
|
||||
|
|
@ -108,8 +104,8 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
|
|||
j = yRange.divideCount - j - 1
|
||||
y := divideValues[j]
|
||||
y += margin
|
||||
if i != 0 {
|
||||
y += i * (barHeight + barMargin)
|
||||
if index != 0 {
|
||||
y += index * (barHeight + barMargin)
|
||||
}
|
||||
|
||||
w := int(xRange.getHeight(item.Value))
|
||||
|
|
|
|||
21
legend.go
21
legend.go
|
|
@ -56,6 +56,8 @@ type LegendOption struct {
|
|||
FontSize float64
|
||||
// FontColor color of legend text
|
||||
FontColor Color
|
||||
// The flag for show legend, set this to *false will hide legend
|
||||
Show *bool
|
||||
}
|
||||
|
||||
func NewLegendOption(labels []string, left ...string) LegendOption {
|
||||
|
|
@ -68,6 +70,17 @@ func NewLegendOption(labels []string, left ...string) LegendOption {
|
|||
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 {
|
||||
return &legendPainter{
|
||||
p: p,
|
||||
|
|
@ -78,6 +91,10 @@ func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
|||
func (l *legendPainter) Render() (Box, error) {
|
||||
opt := l.opt
|
||||
theme := opt.Theme
|
||||
if opt.IsEmpty() ||
|
||||
(opt.Show != nil && !*opt.Show) {
|
||||
return BoxZero, nil
|
||||
}
|
||||
if theme == nil {
|
||||
theme = l.p.theme
|
||||
}
|
||||
|
|
@ -90,7 +107,9 @@ func (l *legendPainter) Render() (Box, error) {
|
|||
if opt.Left == "" {
|
||||
opt.Left = PositionCenter
|
||||
}
|
||||
p := l.p
|
||||
p := l.p.Child(PainterPaddingOption(Box{
|
||||
Top: 5,
|
||||
}))
|
||||
p.SetTextStyle(Style{
|
||||
FontSize: opt.FontSize,
|
||||
FontColor: opt.FontColor,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
|
|||
}
|
||||
for index := range seriesList {
|
||||
series := seriesList[index]
|
||||
seriesColor := opt.Theme.GetSeriesColor(index)
|
||||
seriesColor := opt.Theme.GetSeriesColor(series.index)
|
||||
drawingStyle := Style{
|
||||
StrokeColor: seriesColor,
|
||||
StrokeWidth: defaultStrokeWidth,
|
||||
|
|
|
|||
|
|
@ -628,6 +628,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
|||
values = autoDivide(width, count)
|
||||
}
|
||||
for index, text := range opt.TextList {
|
||||
if index%opt.Unit != 0 {
|
||||
continue
|
||||
}
|
||||
box := p.MeasureText(text)
|
||||
start := values[index]
|
||||
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
|
||||
// The font size of x axis label
|
||||
FontSize float64
|
||||
// Hidden x axis
|
||||
Hidden bool
|
||||
// The flag for show axis, set this to *false will hide axis
|
||||
Show *bool
|
||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||
SplitNumber int
|
||||
// The position of axis, it can be 'top' or 'bottom'
|
||||
|
|
@ -78,6 +78,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
|
|||
FontSize: opt.FontSize,
|
||||
Font: opt.Font,
|
||||
FontColor: opt.FontColor,
|
||||
Show: opt.Show,
|
||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||
}
|
||||
if opt.isValueAxis {
|
||||
|
|
|
|||
28
yaxis.go
28
yaxis.go
|
|
@ -25,6 +25,10 @@ package charts
|
|||
import "github.com/golang/freetype/truetype"
|
||||
|
||||
type YAxisOption struct {
|
||||
// The minimun value of axis.
|
||||
Min *float64
|
||||
// The maximum value of axis.
|
||||
Max *float64
|
||||
// The font of y axis
|
||||
Font *truetype.Font
|
||||
// The data value of x axis
|
||||
|
|
@ -36,7 +40,13 @@ type YAxisOption struct {
|
|||
// The position of axis, it can be 'left' or 'right'
|
||||
Position string
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +70,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
|||
position = PositionRight
|
||||
}
|
||||
axisOpt := AxisOption{
|
||||
Formatter: opt.Formatter,
|
||||
Theme: opt.Theme,
|
||||
Data: opt.Data,
|
||||
Position: position,
|
||||
|
|
@ -70,6 +81,11 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
|
|||
BoundaryGap: FalseFlag(),
|
||||
SplitLineShow: true,
|
||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||
Show: opt.Show,
|
||||
}
|
||||
if !opt.Color.IsZero() {
|
||||
axisOpt.FontColor = opt.Color
|
||||
axisOpt.StrokeColor = opt.Color
|
||||
}
|
||||
if opt.isCategoryAxis {
|
||||
axisOpt.BoundaryGap = TrueFlag()
|
||||
|
|
@ -85,3 +101,13 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
|||
}))
|
||||
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