go-chart/donut_chart.go
Anders Bach Madsen 69b6cb7fc5 Use ArcTo to draw the single value case.
Use the ArcTo method to draw the single value case.
Rename pc (piechart) to dc (donutchart)
2020-05-26 15:47:22 +02:00

329 lines
8 KiB
Go

package chart
import (
"errors"
"fmt"
"io"
"github.com/golang/freetype/truetype"
)
// DonutChart is a chart that draws sections of a circle based on percentages with an hole.
type DonutChart struct {
Title string
TitleStyle Style
ColorPalette ColorPalette
Width int
Height int
DPI float64
Background Style
Canvas Style
SliceStyle Style
Font *truetype.Font
defaultFont *truetype.Font
Values []Value
Elements []Renderable
}
// GetDPI returns the dpi for the chart.
func (dc DonutChart) GetDPI(defaults ...float64) float64 {
if dc.DPI == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultDPI
}
return dc.DPI
}
// GetFont returns the text font.
func (dc DonutChart) GetFont() *truetype.Font {
if dc.Font == nil {
return dc.defaultFont
}
return dc.Font
}
// GetWidth returns the chart width or the default value.
func (dc DonutChart) GetWidth() int {
if dc.Width == 0 {
return DefaultChartWidth
}
return dc.Width
}
// GetHeight returns the chart height or the default value.
func (dc DonutChart) GetHeight() int {
if dc.Height == 0 {
return DefaultChartWidth
}
return dc.Height
}
// Render renders the chart with the given renderer to the given io.Writer.
func (dc DonutChart) Render(rp RendererProvider, w io.Writer) error {
if len(dc.Values) == 0 {
return errors.New("please provide at least one value")
}
r, err := rp(dc.GetWidth(), dc.GetHeight())
if err != nil {
return err
}
if dc.Font == nil {
defaultFont, err := GetDefaultFont()
if err != nil {
return err
}
dc.defaultFont = defaultFont
}
r.SetDPI(dc.GetDPI(DefaultDPI))
canvasBox := dc.getDefaultCanvasBox()
canvasBox = dc.getCircleAdjustedCanvasBox(canvasBox)
dc.drawBackground(r)
dc.drawCanvas(r, canvasBox)
finalValues, err := dc.finalizeValues(dc.Values)
if err != nil {
return err
}
dc.drawSlices(r, canvasBox, finalValues)
dc.drawTitle(r)
for _, a := range dc.Elements {
a(r, canvasBox, dc.styleDefaultsElements())
}
return r.Save(w)
}
func (dc DonutChart) drawBackground(r Renderer) {
Draw.Box(r, Box{
Right: dc.GetWidth(),
Bottom: dc.GetHeight(),
}, dc.getBackgroundStyle())
}
func (dc DonutChart) drawCanvas(r Renderer, canvasBox Box) {
Draw.Box(r, canvasBox, dc.getCanvasStyle())
}
func (dc DonutChart) drawTitle(r Renderer) {
if len(dc.Title) > 0 && !dc.TitleStyle.Hidden {
Draw.TextWithin(r, dc.Title, dc.Box(), dc.styleDefaultsTitle())
}
}
func (dc DonutChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
cx, cy := canvasBox.Center()
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
radius := float64(diameter>>1) / 1.1
labelRadius := (radius * 2.83) / 3.0
// draw the donut slices
var rads, delta, delta2, total float64
var lx, ly int
if len(values) == 1 {
dc.styleDonutChartValueSingle(0).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.ArcTo(cx, cy, (radius / 1.25), (radius / 1.25), DegreesToRadians(0), DegreesToRadians(359))
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
} else {
for index, v := range values {
v.Style.InheritFrom(dc.styleDonutChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy)
rads = PercentToRadians(total)
delta = PercentToRadians(v.Value)
r.ArcTo(cx, cy, (radius / 1.25), (radius / 1.25), rads, delta)
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
total = total + v.Value
}
}
//making the donut hole
v := Value{Value: 100, Label: "center"}
styletemp := dc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite, StrokeWidth: 4.0, FillColor: ColorWhite, FontColor: ColorWhite,
})
v.Style.InheritFrom(styletemp).WriteToRenderer(r)
r.MoveTo(cx, cy)
r.ArcTo(cx, cy, (radius / 3.5), (radius / 3.5), DegreesToRadians(0), DegreesToRadians(359))
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
// draw the labels
total = 0
for index, v := range values {
v.Style.InheritFrom(dc.styleDonutChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2)
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
ly = ly + (tb.Height() >> 1)
r.Text(v.Label, lx, ly)
}
total = total + v.Value
}
}
func (dc DonutChart) finalizeValues(values []Value) ([]Value, error) {
finalValues := Values(values).Normalize()
if len(finalValues) == 0 {
return nil, fmt.Errorf("donut chart must contain at least (1) non-zero value")
}
return finalValues, nil
}
func (dc DonutChart) getDefaultCanvasBox() Box {
return dc.Box()
}
func (dc DonutChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
square := Box{
Right: circleDiameter,
Bottom: circleDiameter,
}
return canvasBox.Fit(square)
}
func (dc DonutChart) getBackgroundStyle() Style {
return dc.Background.InheritFrom(dc.styleDefaultsBackground())
}
func (dc DonutChart) getCanvasStyle() Style {
return dc.Canvas.InheritFrom(dc.styleDefaultsCanvas())
}
func (dc DonutChart) styleDefaultsCanvas() Style {
return Style{
FillColor: dc.GetColorPalette().CanvasColor(),
StrokeColor: dc.GetColorPalette().CanvasStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (dc DonutChart) styleDefaultsDonutChartValue() Style {
return Style{
StrokeColor: dc.GetColorPalette().TextColor(),
StrokeWidth: 4.0,
FillColor: dc.GetColorPalette().TextColor(),
}
}
func (dc DonutChart) styleDonutChartValue(index int) Style {
return dc.SliceStyle.InheritFrom(Style{
StrokeColor: ColorWhite,
StrokeWidth: 4.0,
FillColor: dc.GetColorPalette().GetSeriesColor(index),
FontSize: dc.getScaledFontSize(),
FontColor: dc.GetColorPalette().TextColor(),
Font: dc.GetFont(),
})
}
func (dc DonutChart) styleDonutChartValueSingle(index int) Style {
return dc.SliceStyle.InheritFrom(Style{
StrokeColor: dc.GetColorPalette().GetSeriesColor(index),
StrokeWidth: 4.0,
FillColor: dc.GetColorPalette().GetSeriesColor(index),
FontSize: dc.getScaledFontSize(),
FontColor: dc.GetColorPalette().TextColor(),
Font: dc.GetFont(),
})
}
func (dc DonutChart) getScaledFontSize() float64 {
effectiveDimension := MinInt(dc.GetWidth(), dc.GetHeight())
if effectiveDimension >= 2048 {
return 48.0
} else if effectiveDimension >= 1024 {
return 24.0
} else if effectiveDimension > 512 {
return 18.0
} else if effectiveDimension > 256 {
return 12.0
}
return 10.0
}
func (dc DonutChart) styleDefaultsBackground() Style {
return Style{
FillColor: dc.GetColorPalette().BackgroundColor(),
StrokeColor: dc.GetColorPalette().BackgroundStrokeColor(),
StrokeWidth: DefaultStrokeWidth,
}
}
func (dc DonutChart) styleDefaultsElements() Style {
return Style{
Font: dc.GetFont(),
}
}
func (dc DonutChart) styleDefaultsTitle() Style {
return dc.TitleStyle.InheritFrom(Style{
FontColor: dc.GetColorPalette().TextColor(),
Font: dc.GetFont(),
FontSize: dc.getTitleFontSize(),
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapWord,
})
}
func (dc DonutChart) getTitleFontSize() float64 {
effectiveDimension := MinInt(dc.GetWidth(), dc.GetHeight())
if effectiveDimension >= 2048 {
return 48
} else if effectiveDimension >= 1024 {
return 24
} else if effectiveDimension >= 512 {
return 18
} else if effectiveDimension >= 256 {
return 12
}
return 10
}
// GetColorPalette returns the color palette for the chart.
func (dc DonutChart) GetColorPalette() ColorPalette {
if dc.ColorPalette != nil {
return dc.ColorPalette
}
return AlternateColorPalette
}
// Box returns the chart bounds as a box.
func (dc DonutChart) Box() Box {
dpr := dc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := dc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: dc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
Left: dc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
Right: dc.GetWidth() - dpr,
Bottom: dc.GetHeight() - dpb,
}
}