285 lines
6.4 KiB
Go
285 lines
6.4 KiB
Go
|
package chart
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"io"
|
||
|
"math"
|
||
|
|
||
|
"github.com/golang/freetype/truetype"
|
||
|
)
|
||
|
|
||
|
// PieChartValue is a slice of a pie-chart.
|
||
|
type PieChartValue struct {
|
||
|
Style Style
|
||
|
Label string
|
||
|
Value float64
|
||
|
}
|
||
|
|
||
|
// PieChart is a chart that draws sections of a circle based on percentages.
|
||
|
type PieChart struct {
|
||
|
Title string
|
||
|
TitleStyle Style
|
||
|
|
||
|
Width int
|
||
|
Height int
|
||
|
DPI float64
|
||
|
|
||
|
Background Style
|
||
|
Canvas Style
|
||
|
|
||
|
Font *truetype.Font
|
||
|
defaultFont *truetype.Font
|
||
|
|
||
|
Values []PieChartValue
|
||
|
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 {
|
||
|
return errors.New("Please provide at least one value.")
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
|
||
|
valuesWithPlaceholder, err := pc.finalizeValues(pc.Values)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
pc.drawSlices(r, canvasBox, valuesWithPlaceholder)
|
||
|
pc.drawTitle(r)
|
||
|
for _, a := range pc.Elements {
|
||
|
a(r, canvasBox, pc.styleDefaultsElements())
|
||
|
}
|
||
|
|
||
|
return r.Save(w)
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) drawBackground(r Renderer) {
|
||
|
DrawBox(r, Box{
|
||
|
Right: pc.GetWidth(),
|
||
|
Bottom: pc.GetHeight(),
|
||
|
}, pc.getBackgroundStyle())
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
|
||
|
DrawBox(r, canvasBox, pc.getCanvasStyle())
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) drawTitle(r Renderer) {
|
||
|
if len(pc.Title) > 0 && pc.TitleStyle.Show {
|
||
|
r.SetFont(pc.TitleStyle.GetFont(pc.GetFont()))
|
||
|
r.SetFontColor(pc.TitleStyle.GetFontColor(DefaultTextColor))
|
||
|
titleFontSize := pc.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||
|
r.SetFontSize(titleFontSize)
|
||
|
|
||
|
textBox := r.MeasureText(pc.Title)
|
||
|
|
||
|
textWidth := textBox.Width()
|
||
|
textHeight := textBox.Height()
|
||
|
|
||
|
titleX := (pc.GetWidth() >> 1) - (textWidth >> 1)
|
||
|
titleY := pc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||
|
|
||
|
r.Text(pc.Title, titleX, titleY)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) {
|
||
|
cx, cy := canvasBox.Center()
|
||
|
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||
|
radius := float64(diameter >> 1)
|
||
|
radius2 := (radius * 2.0) / 3.0
|
||
|
|
||
|
var rads, delta, delta2, total float64
|
||
|
var lx, ly int
|
||
|
for index, v := range values {
|
||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r)
|
||
|
r.MoveTo(cx, cy)
|
||
|
|
||
|
rads = PercentToRadians(total)
|
||
|
delta = PercentToRadians(v.Value)
|
||
|
|
||
|
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||
|
|
||
|
r.LineTo(cx, cy)
|
||
|
r.Close()
|
||
|
r.FillStroke()
|
||
|
total = total + v.Value
|
||
|
}
|
||
|
|
||
|
total = 0
|
||
|
for index, v := range values {
|
||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r)
|
||
|
if len(v.Label) > 0 {
|
||
|
delta2 = RadianAdd(PercentToRadians(total+(v.Value/2.0)), _pi2)
|
||
|
lx = cx + int(radius2*math.Sin(delta2))
|
||
|
ly = cy - int(radius2*math.Cos(delta2))
|
||
|
|
||
|
tb := r.MeasureText(v.Label)
|
||
|
lx = lx - (tb.Width() >> 1)
|
||
|
|
||
|
r.Text(v.Label, lx, ly)
|
||
|
}
|
||
|
total = total + v.Value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) finalizeValues(values []PieChartValue) ([]PieChartValue, error) {
|
||
|
var total float64
|
||
|
for _, v := range values {
|
||
|
total += v.Value
|
||
|
if total > 1.0 {
|
||
|
return nil, errors.New("Values total exceeded 1.0; please normalize pie chart values to [0,1.0)")
|
||
|
}
|
||
|
}
|
||
|
remainder := 1.0 - total
|
||
|
if RoundDown(remainder, 0.0001) > 0 {
|
||
|
return append(values, PieChartValue{
|
||
|
Style: pc.styleDefaultsPieChartValue(),
|
||
|
Value: remainder,
|
||
|
}), nil
|
||
|
}
|
||
|
return values, nil
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) getDefaultCanvasBox() Box {
|
||
|
return pc.Box()
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
|
||
|
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||
|
|
||
|
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{
|
||
|
FillColor: DefaultCanvasColor,
|
||
|
StrokeColor: DefaultCanvasStrokeColor,
|
||
|
StrokeWidth: DefaultStrokeWidth,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) styleDefaultsPieChartValue() Style {
|
||
|
return Style{
|
||
|
StrokeColor: ColorWhite,
|
||
|
StrokeWidth: 5.0,
|
||
|
FillColor: ColorWhite,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) stylePieChartValue(index int) Style {
|
||
|
return Style{
|
||
|
StrokeColor: ColorWhite,
|
||
|
StrokeWidth: 5.0,
|
||
|
FillColor: GetDefaultPieChartValueColor(index),
|
||
|
FontSize: 24.0,
|
||
|
FontColor: ColorWhite,
|
||
|
Font: pc.GetFont(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) styleDefaultsBackground() Style {
|
||
|
return Style{
|
||
|
FillColor: DefaultBackgroundColor,
|
||
|
StrokeColor: DefaultBackgroundStrokeColor,
|
||
|
StrokeWidth: DefaultStrokeWidth,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) styleDefaultsSeries(seriesIndex int) Style {
|
||
|
strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
|
||
|
return Style{
|
||
|
StrokeColor: strokeColor,
|
||
|
StrokeWidth: DefaultStrokeWidth,
|
||
|
Font: pc.GetFont(),
|
||
|
FontSize: DefaultFontSize,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (pc PieChart) styleDefaultsElements() Style {
|
||
|
return Style{
|
||
|
Font: pc.GetFont(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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,
|
||
|
}
|
||
|
}
|