// Copyright ©2015 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package plotter import ( "errors" "image/color" "math" "gonum.org/v1/plot" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" ) // A BarChart presents grouped data with rectangular bars // with lengths proportional to the data values. type BarChart struct { Values // Width is the width of the bars. Width vg.Length // Color is the fill color of the bars. Color color.Color // LineStyle is the style of the outline of the bars. draw.LineStyle // Offset is added to the X location of each bar. // When the Offset is zero, the bars are drawn // centered at their X location. Offset vg.Length // XMin is the X location of the first bar. XMin // can be changed to move groups of bars // down the X axis in order to make grouped // bar charts. XMin float64 // Horizontal dictates whether the bars should be in the vertical // (default) or horizontal direction. If Horizontal is true, all // X locations and distances referred to here will actually be Y // locations and distances. Horizontal bool // stackedOn is the bar chart upon which // this bar chart is stacked. stackedOn *BarChart } // NewBarChart returns a new bar chart with a single bar for each value. // The bars heights correspond to the values and their x locations correspond // to the index of their value in the Valuer. func NewBarChart(vs Valuer, width vg.Length) (*BarChart, error) { if width <= 0 { return nil, errors.New("plotter: width parameter was not positive") } values, err := CopyValues(vs) if err != nil { return nil, err } return &BarChart{ Values: values, Width: width, Color: color.Black, LineStyle: DefaultLineStyle, }, nil } // BarHeight returns the maximum y value of the // ith bar, taking into account any bars upon // which it is stacked. func (b *BarChart) BarHeight(i int) float64 { ht := 0.0 if b == nil { return 0 } if i >= 0 && i < len(b.Values) { ht += b.Values[i] } if b.stackedOn != nil { ht += b.stackedOn.BarHeight(i) } return ht } // StackOn stacks a bar chart on top of another, // and sets the XMin and Offset to that of the // chart upon which it is being stacked. func (b *BarChart) StackOn(on *BarChart) { b.XMin = on.XMin b.Offset = on.Offset b.stackedOn = on } // Plot implements the plot.Plotter interface. func (b *BarChart) Plot(c draw.Canvas, plt *plot.Plot) { trCat, trVal := plt.Transforms(&c) if b.Horizontal { trCat, trVal = trVal, trCat } for i, ht := range b.Values { catVal := b.XMin + float64(i) catMin := trCat(float64(catVal)) if !b.Horizontal { if !c.ContainsX(catMin) { continue } } else { if !c.ContainsY(catMin) { continue } } catMin = catMin - b.Width/2 + b.Offset catMax := catMin + b.Width bottom := b.stackedOn.BarHeight(i) valMin := trVal(bottom) valMax := trVal(bottom + ht) var pts []vg.Point var poly []vg.Point if !b.Horizontal { pts = []vg.Point{ {X: catMin, Y: valMin}, {X: catMin, Y: valMax}, {X: catMax, Y: valMax}, {X: catMax, Y: valMin}, } poly = c.ClipPolygonY(pts) } else { pts = []vg.Point{ {X: valMin, Y: catMin}, {X: valMin, Y: catMax}, {X: valMax, Y: catMax}, {X: valMax, Y: catMin}, } poly = c.ClipPolygonX(pts) } c.FillPolygon(b.Color, poly) var outline [][]vg.Point if !b.Horizontal { pts = append(pts, vg.Point{X: catMin, Y: valMin}) outline = c.ClipLinesY(pts) } else { pts = append(pts, vg.Point{X: valMin, Y: catMin}) outline = c.ClipLinesX(pts) } c.StrokeLines(b.LineStyle, outline...) } } // DataRange implements the plot.DataRanger interface. func (b *BarChart) DataRange() (xmin, xmax, ymin, ymax float64) { catMin := b.XMin catMax := catMin + float64(len(b.Values)-1) valMin := math.Inf(1) valMax := math.Inf(-1) for i, val := range b.Values { valBot := b.stackedOn.BarHeight(i) valTop := valBot + val valMin = math.Min(valMin, math.Min(valBot, valTop)) valMax = math.Max(valMax, math.Max(valBot, valTop)) } if !b.Horizontal { return catMin, catMax, valMin, valMax } return valMin, valMax, catMin, catMax } // GlyphBoxes implements the GlyphBoxer interface. func (b *BarChart) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox { boxes := make([]plot.GlyphBox, len(b.Values)) for i := range b.Values { cat := b.XMin + float64(i) if !b.Horizontal { boxes[i].X = plt.X.Norm(cat) boxes[i].Rectangle = vg.Rectangle{ Min: vg.Point{X: b.Offset - b.Width/2}, Max: vg.Point{X: b.Offset + b.Width/2}, } } else { boxes[i].Y = plt.Y.Norm(cat) boxes[i].Rectangle = vg.Rectangle{ Min: vg.Point{Y: b.Offset - b.Width/2}, Max: vg.Point{Y: b.Offset + b.Width/2}, } } } return boxes } // Thumbnail fulfills the plot.Thumbnailer interface. func (b *BarChart) Thumbnail(c *draw.Canvas) { pts := []vg.Point{ {X: c.Min.X, Y: c.Min.Y}, {X: c.Min.X, Y: c.Max.Y}, {X: c.Max.X, Y: c.Max.Y}, {X: c.Max.X, Y: c.Min.Y}, } poly := c.ClipPolygonY(pts) c.FillPolygon(b.Color, poly) pts = append(pts, vg.Point{X: c.Min.X, Y: c.Min.Y}) outline := c.ClipLinesY(pts) c.StrokeLines(b.LineStyle, outline...) }