From eb45c6479effc088bad52717f6aec16578b533d2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 3 Feb 2022 21:00:01 +0800 Subject: [PATCH] feat: pie chart render function --- bar_chart.go | 9 ++-- chart.go | 49 ++++++++++++++++------ draw.go | 4 ++ line_chart.go | 9 ++-- pie_chart.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ xaxis.go | 1 + yaxis.go | 5 ++- 7 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 pie_chart.go diff --git a/bar_chart.go b/bar_chart.go index 7fd068f..dfaf50f 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -27,10 +27,9 @@ import ( ) func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) { - d := result.d - bd, err := NewDraw(DrawOption{ - Parent: d, + d, err := NewDraw(DrawOption{ + Parent: result.d, }, PaddingOption(chart.Box{ Top: result.titleBox.Height(), Left: YAxisWidth, @@ -65,7 +64,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) { h := int(yRange.getHeight(item.Value)) - bd.Bar(chart.Box{ + d.Bar(chart.Box{ Top: barMaxHeight - h, Left: x, Right: x + barWidth, @@ -76,5 +75,5 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) { } } - return d, nil + return result.d, nil } diff --git a/chart.go b/chart.go index 12af36e..426bc94 100644 --- a/chart.go +++ b/chart.go @@ -23,6 +23,7 @@ package charts import ( + "errors" "math" "github.com/dustin/go-humanize" @@ -168,21 +169,37 @@ type basicRenderResult struct { } func ChartRender(opt ChartOption) (*Draw, error) { - result, err := chartBasicRender(&opt) - if err != nil { - return nil, err + if len(opt.SeriesList) == 0 { + return nil, errors.New("series can not be nil") } + lineSeries := make([]Series, 0) barSeries := make([]Series, 0) + isPieChart := false for index, item := range opt.SeriesList { item.index = index switch item.Type { + case ChartTypePie: + isPieChart = true case ChartTypeBar: barSeries = append(barSeries, item) default: lineSeries = append(lineSeries, item) } } + // 如果指定了pie,则以pie的形式处理,不支持多类型图表 + // pie不需要axis + if isPieChart { + opt.XAxis.Hidden = true + opt.YAxis.Hidden = true + } + result, err := chartBasicRender(&opt) + if err != nil { + return nil, err + } + if isPieChart { + return pieChartRender(opt, result) + } if len(barSeries) != 0 { o := opt o.SeriesList = barSeries @@ -231,18 +248,26 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { return nil, err } - // xAxis - xAxisHeight, xRange, err := drawXAxis(d, &opt.XAxis) - if err != nil { - return nil, err + xAxisHeight := 0 + var xRange *Range + + if !opt.XAxis.Hidden { + // xAxis + xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis) + if err != nil { + return nil, err + } } // 暂时仅支持单一yaxis - yRange, err := drawYAxis(d, opt, xAxisHeight, chart.Box{ - Top: titleBox.Height(), - }) - if err != nil { - return nil, err + var yRange *Range + if !opt.YAxis.Hidden { + yRange, err = drawYAxis(d, opt, xAxisHeight, chart.Box{ + Top: titleBox.Height(), + }) + if err != nil { + return nil, err + } } return &basicRenderResult{ xRange: xRange, diff --git a/draw.go b/draw.go index 037124b..ce0bd6b 100644 --- a/draw.go +++ b/draw.go @@ -138,6 +138,10 @@ func (d *Draw) moveTo(x, y int) { d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top) } +func (d *Draw) arcTo(cx, cy int, rx, ry, startAngle, delta float64) { + d.Render.ArcTo(cx+d.Box.Left, cy+d.Box.Top, rx, ry, startAngle, delta) +} + func (d *Draw) lineTo(x, y int) { d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) } diff --git a/line_chart.go b/line_chart.go index 4f10f97..cf769e8 100644 --- a/line_chart.go +++ b/line_chart.go @@ -29,11 +29,10 @@ import ( func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) { - d := result.d theme := NewTheme(opt.Theme) - sd, err := NewDraw(DrawOption{ - Parent: d, + d, err := NewDraw(DrawOption{ + Parent: result.d, }, PaddingOption(chart.Box{ Top: result.titleBox.Height(), Left: YAxisWidth, @@ -60,7 +59,7 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) if theme.IsDark() { dotFillColor = seriesColor } - sd.Line(points, LineStyle{ + d.Line(points, LineStyle{ StrokeColor: seriesColor, StrokeWidth: 2, DotColor: seriesColor, @@ -70,5 +69,5 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) } } - return d, nil + return result.d, nil } diff --git a/pie_chart.go b/pie_chart.go new file mode 100644 index 0000000..1013bce --- /dev/null +++ b/pie_chart.go @@ -0,0 +1,111 @@ +// 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 ( + "math" + + "github.com/wcharczuk/go-chart/v2" +) + +const defaultRadiusPercent = 0.4 + +func getPieStyle(theme *Theme, index int) chart.Style { + seriesColor := theme.GetSeriesColor(index) + return chart.Style{ + StrokeColor: seriesColor, + StrokeWidth: 1, + FillColor: seriesColor, + } + +} + +func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) { + d, err := NewDraw(DrawOption{ + Parent: result.d, + }, PaddingOption(chart.Box{ + Top: result.titleBox.Height(), + })) + if err != nil { + return nil, err + } + + values := make([]float64, len(opt.SeriesList)) + total := float64(0) + for index, series := range opt.SeriesList { + value := float64(0) + for _, item := range series.Data { + value += item.Value + } + values[index] = value + total += value + } + r := d.Render + theme := NewTheme(opt.Theme) + + box := d.Box + cx := box.Width() >> 1 + cy := box.Height() >> 1 + + diameter := chart.MinInt(box.Width(), box.Height()) + radius := float64(diameter) * defaultRadiusPercent + labelRadius := radius + 20 + + if len(values) == 1 { + getPieStyle(theme, 0).WriteToRenderer(r) + d.moveTo(cx, cy) + d.circle(radius, cx, cy) + } else { + currentValue := float64(0) + for index, v := range values { + getPieStyle(theme, index).WriteToRenderer(r) + d.moveTo(cx, cy) + start := chart.PercentToRadians(currentValue/total) - math.Pi/2 + currentValue += v + delta := chart.PercentToRadians(v / total) + d.arcTo(cx, cy, radius, radius, start, delta) + d.lineTo(cx, cy) + r.Close() + r.FillStroke() + + // 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)) + d.moveTo(startx, starty) + d.lineTo(endx, endy) + offset := 30 + if endx < cx { + offset *= -1 + } + d.moveTo(endx, endy) + d.lineTo(endx+offset, endy) + r.Stroke() + // TODO label show + } + } + return result.d, nil +} diff --git a/xaxis.go b/xaxis.go index 6839329..8d47ed8 100644 --- a/xaxis.go +++ b/xaxis.go @@ -28,6 +28,7 @@ type XAxisOption struct { BoundaryGap *bool Data []string Theme string + Hidden bool // TODO split number } diff --git a/yaxis.go b/yaxis.go index 7329804..6b8af4c 100644 --- a/yaxis.go +++ b/yaxis.go @@ -27,8 +27,9 @@ import ( ) type YAxisOption struct { - Min *float64 - Max *float64 + Min *float64 + Max *float64 + Hidden bool } const YAxisWidth = 40