go-chart/pie_chart.go

320 lines
7.3 KiB
Go
Raw Normal View History

2016-07-28 11:34:44 +02:00
package chart
import (
"errors"
2017-01-13 23:07:42 +01:00
"fmt"
2016-07-28 11:34:44 +02:00
"io"
"math"
2016-07-28 11:34:44 +02:00
"git.fireandbrimst.one/aw/go-chart/util"
"git.fireandbrimst.one/aw/golang-freetype/truetype"
)
const (
2017-10-12 22:29:55 +02:00
_pi = math.Pi
_pi2 = math.Pi / 2.0
_pi4 = math.Pi / 4.0
2016-07-28 11:34:44 +02:00
)
// PieChart is a chart that draws sections of a circle based on percentages.
type PieChart struct {
Title string
TitleStyle Style
2017-04-15 02:43:52 +02:00
ColorPalette ColorPalette
2016-07-28 11:34:44 +02:00
Width int
Height int
DPI float64
Background Style
Canvas Style
2016-07-29 04:17:35 +02:00
SliceStyle Style
2016-07-28 11:34:44 +02:00
Font *truetype.Font
defaultFont *truetype.Font
2016-07-28 23:30:00 +02:00
Values []Value
2016-07-28 11:34:44 +02:00
Elements []Renderable
}
// GetDPI returns the dpi for the chart.
func (pc PieChart) GetDPI(defaults ...float64) float64 {
if pc.DPI == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultDPI
}
return pc.DPI
}
// GetFont returns the text font.
func (pc PieChart) GetFont() *truetype.Font {
if pc.Font == nil {
return pc.defaultFont
}
return pc.Font
}
// GetWidth returns the chart width or the default value.
func (pc PieChart) GetWidth() int {
if pc.Width == 0 {
return DefaultChartWidth
}
return pc.Width
}
// GetHeight returns the chart height or the default value.
func (pc PieChart) GetHeight() int {
if pc.Height == 0 {
return DefaultChartWidth
}
return pc.Height
}
// Render renders the chart with the given renderer to the given io.Writer.
func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
if len(pc.Values) == 0 {
2017-02-03 20:26:53 +01:00
return errors.New("please provide at least one value")
2016-07-28 11:34:44 +02:00
}
r, err := rp(pc.GetWidth(), pc.GetHeight())
if err != nil {
return err
}
if pc.Font == nil {
defaultFont, err := GetDefaultFont()
if err != nil {
return err
}
pc.defaultFont = defaultFont
}
r.SetDPI(pc.GetDPI(DefaultDPI))
canvasBox := pc.getDefaultCanvasBox()
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
pc.drawBackground(r)
pc.drawCanvas(r, canvasBox)
2017-01-13 23:07:42 +01:00
finalValues, err := pc.finalizeValues(pc.Values)
if err != nil {
return err
}
2016-07-28 23:30:00 +02:00
pc.drawSlices(r, canvasBox, finalValues)
2016-07-28 11:34:44 +02:00
pc.drawTitle(r)
for _, a := range pc.Elements {
a(r, canvasBox, pc.styleDefaultsElements())
}
return r.Save(w)
}
func (pc PieChart) drawBackground(r Renderer) {
2016-07-30 01:36:29 +02:00
Draw.Box(r, Box{
2016-07-28 11:34:44 +02:00
Right: pc.GetWidth(),
Bottom: pc.GetHeight(),
}, pc.getBackgroundStyle())
}
func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
2016-07-30 01:36:29 +02:00
Draw.Box(r, canvasBox, pc.getCanvasStyle())
2016-07-28 11:34:44 +02:00
}
func (pc PieChart) drawTitle(r Renderer) {
if len(pc.Title) > 0 && pc.TitleStyle.Show {
2016-07-30 03:24:25 +02:00
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
2016-07-28 11:34:44 +02:00
}
}
2016-07-28 23:30:00 +02:00
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
2016-07-28 11:34:44 +02:00
cx, cy := canvasBox.Center()
diameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height())
2016-07-28 11:34:44 +02:00
radius := float64(diameter >> 1)
2016-07-28 22:22:18 +02:00
labelRadius := (radius * 2.0) / 3.0
2016-07-28 11:34:44 +02:00
2016-07-28 22:22:18 +02:00
// draw the pie slices
2016-07-28 11:34:44 +02:00
var rads, delta, delta2, total float64
var lx, ly int
if len(values) == 1 {
pc.stylePieChartValue(0).WriteToRenderer(r)
2016-07-28 22:22:18 +02:00
r.MoveTo(cx, cy)
r.Circle(radius, cx, cy)
} else {
for index, v := range values {
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
2016-07-28 11:34:44 +02:00
r.MoveTo(cx, cy)
rads = util.Math.PercentToRadians(total)
delta = util.Math.PercentToRadians(v.Value)
2016-07-28 11:34:44 +02:00
r.ArcTo(cx, cy, radius, radius, rads, delta)
r.LineTo(cx, cy)
r.Close()
r.FillStroke()
total = total + v.Value
}
2016-07-28 11:34:44 +02:00
}
2016-07-28 22:22:18 +02:00
// draw the labels
2016-07-28 11:34:44 +02:00
total = 0
for index, v := range values {
2016-07-30 01:36:29 +02:00
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
2016-07-28 11:34:44 +02:00
if len(v.Label) > 0 {
delta2 = util.Math.PercentToRadians(total + (v.Value / 2.0))
delta2 = util.Math.RadianAdd(delta2, _pi2)
lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
2016-07-28 11:34:44 +02:00
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
2016-07-28 23:30:00 +02:00
ly = ly + (tb.Height() >> 1)
2016-07-28 11:34:44 +02:00
if lx < 0 {
lx = 0
}
if ly < 0 {
lx = 0
}
2016-07-28 11:34:44 +02:00
r.Text(v.Label, lx, ly)
}
total = total + v.Value
}
}
2017-01-13 23:07:42 +01:00
func (pc PieChart) finalizeValues(values []Value) ([]Value, error) {
finalValues := Values(values).Normalize()
if len(finalValues) == 0 {
return nil, fmt.Errorf("pie chart must contain at least (1) non-zero value")
}
return finalValues, nil
2016-07-28 11:34:44 +02:00
}
func (pc PieChart) getDefaultCanvasBox() Box {
return pc.Box()
}
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height())
2016-07-28 11:34:44 +02:00
square := Box{
Right: circleDiameter,
Bottom: circleDiameter,
}
return canvasBox.Fit(square)
}
func (pc PieChart) getBackgroundStyle() Style {
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
}
func (pc PieChart) getCanvasStyle() Style {
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
}
func (pc PieChart) styleDefaultsCanvas() Style {
return Style{
2017-04-15 02:43:52 +02:00
FillColor: pc.GetColorPalette().CanvasColor(),
StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
2016-07-28 11:34:44 +02:00
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc PieChart) styleDefaultsPieChartValue() Style {
return Style{
2017-04-15 02:43:52 +02:00
StrokeColor: pc.GetColorPalette().TextColor(),
2016-07-28 11:34:44 +02:00
StrokeWidth: 5.0,
2017-04-15 02:43:52 +02:00
FillColor: pc.GetColorPalette().TextColor(),
2016-07-28 11:34:44 +02:00
}
}
func (pc PieChart) stylePieChartValue(index int) Style {
2016-07-29 04:17:35 +02:00
return pc.SliceStyle.InheritFrom(Style{
2016-07-28 11:34:44 +02:00
StrokeColor: ColorWhite,
StrokeWidth: 5.0,
2017-04-15 02:43:52 +02:00
FillColor: pc.GetColorPalette().GetSeriesColor(index),
2016-07-29 04:17:35 +02:00
FontSize: pc.getScaledFontSize(),
2017-04-15 02:43:52 +02:00
FontColor: pc.GetColorPalette().TextColor(),
2016-07-28 11:34:44 +02:00
Font: pc.GetFont(),
2016-07-29 04:17:35 +02:00
})
}
func (pc PieChart) getScaledFontSize() float64 {
effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight())
2016-07-29 04:17:35 +02:00
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
2016-07-28 11:34:44 +02:00
}
2016-07-29 04:17:35 +02:00
return 10.0
2016-07-28 11:34:44 +02:00
}
func (pc PieChart) styleDefaultsBackground() Style {
return Style{
2017-04-15 02:43:52 +02:00
FillColor: pc.GetColorPalette().BackgroundColor(),
StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
2016-07-28 11:34:44 +02:00
StrokeWidth: DefaultStrokeWidth,
}
}
func (pc PieChart) styleDefaultsElements() Style {
return Style{
Font: pc.GetFont(),
}
}
2016-07-30 03:24:25 +02:00
func (pc PieChart) styleDefaultsTitle() Style {
return pc.TitleStyle.InheritFrom(Style{
2017-04-15 02:43:52 +02:00
FontColor: pc.GetColorPalette().TextColor(),
2016-07-30 03:24:25 +02:00
Font: pc.GetFont(),
2016-07-30 03:40:43 +02:00
FontSize: pc.getTitleFontSize(),
2016-07-30 03:24:25 +02:00
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
2016-07-30 03:40:43 +02:00
TextWrap: TextWrapWord,
2016-07-30 03:24:25 +02:00
})
}
2016-07-30 03:40:43 +02:00
func (pc PieChart) getTitleFontSize() float64 {
effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight())
2016-07-30 03:40:43 +02:00
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
}
2017-04-15 02:43:52 +02:00
// GetColorPalette returns the color palette for the chart.
func (pc PieChart) GetColorPalette() ColorPalette {
if pc.ColorPalette != nil {
return pc.ColorPalette
}
return AlternateColorPalette
}
2016-07-28 11:34:44 +02:00
// Box returns the chart bounds as a box.
func (pc PieChart) Box() Box {
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
return Box{
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
Right: pc.GetWidth() - dpr,
Bottom: pc.GetHeight() - dpb,
}
}