212 lines
5.2 KiB
Go
212 lines
5.2 KiB
Go
// 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...)
|
|
}
|