211 lines
5.6 KiB
Go
211 lines
5.6 KiB
Go
// 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)
|
||
}
|