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, } }