231 lines
5.3 KiB
Go
231 lines
5.3 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"
|
|
"fmt"
|
|
"image/color"
|
|
"math"
|
|
|
|
"gonum.org/v1/plot"
|
|
"gonum.org/v1/plot/vg"
|
|
"gonum.org/v1/plot/vg/draw"
|
|
)
|
|
|
|
// Histogram implements the Plotter interface,
|
|
// drawing a histogram of the data.
|
|
type Histogram struct {
|
|
// Bins is the set of bins for this histogram.
|
|
Bins []HistogramBin
|
|
|
|
// Width is the width of each bin.
|
|
Width float64
|
|
|
|
// FillColor is the color used to fill each
|
|
// bar of the histogram. If the color is nil
|
|
// then the bars are not filled.
|
|
FillColor color.Color
|
|
|
|
// LineStyle is the style of the outline of each
|
|
// bar of the histogram.
|
|
draw.LineStyle
|
|
|
|
// LogY allows rendering with a log-scaled Y axis.
|
|
// When enabled, histogram bins with no entries will be discarded from
|
|
// the histogram's DataRange.
|
|
// The lowest Y value for the DataRange will be corrected to leave an
|
|
// arbitrary amount of height for the smallest bin entry so it is visible
|
|
// on the final plot.
|
|
LogY bool
|
|
}
|
|
|
|
// NewHistogram returns a new histogram
|
|
// that represents the distribution of values
|
|
// using the given number of bins.
|
|
//
|
|
// Each y value is assumed to be the frequency
|
|
// count for the corresponding x.
|
|
//
|
|
// If the number of bins is non-positive than
|
|
// a reasonable default is used.
|
|
func NewHistogram(xy XYer, n int) (*Histogram, error) {
|
|
if n <= 0 {
|
|
return nil, errors.New("Histogram with non-positive number of bins")
|
|
}
|
|
bins, width := binPoints(xy, n)
|
|
return &Histogram{
|
|
Bins: bins,
|
|
Width: width,
|
|
FillColor: color.Gray{128},
|
|
LineStyle: DefaultLineStyle,
|
|
}, nil
|
|
}
|
|
|
|
// NewHist returns a new histogram, as in
|
|
// NewHistogram, except that it accepts a Valuer
|
|
// instead of an XYer.
|
|
func NewHist(vs Valuer, n int) (*Histogram, error) {
|
|
return NewHistogram(unitYs{vs}, n)
|
|
}
|
|
|
|
type unitYs struct {
|
|
Valuer
|
|
}
|
|
|
|
func (u unitYs) XY(i int) (float64, float64) {
|
|
return u.Value(i), 1.0
|
|
}
|
|
|
|
// Plot implements the Plotter interface, drawing a line
|
|
// that connects each point in the Line.
|
|
func (h *Histogram) Plot(c draw.Canvas, p *plot.Plot) {
|
|
trX, trY := p.Transforms(&c)
|
|
|
|
for _, bin := range h.Bins {
|
|
ymin := c.Min.Y
|
|
ymax := c.Min.Y
|
|
if bin.Weight != 0 {
|
|
ymax = trY(bin.Weight)
|
|
}
|
|
xmin := trX(bin.Min)
|
|
xmax := trX(bin.Max)
|
|
pts := []vg.Point{
|
|
{X: xmin, Y: ymin},
|
|
{X: xmax, Y: ymin},
|
|
{X: xmax, Y: ymax},
|
|
{X: xmin, Y: ymax},
|
|
}
|
|
if h.FillColor != nil {
|
|
c.FillPolygon(h.FillColor, c.ClipPolygonXY(pts))
|
|
}
|
|
pts = append(pts, vg.Point{X: xmin, Y: ymin})
|
|
c.StrokeLines(h.LineStyle, c.ClipLinesXY(pts)...)
|
|
}
|
|
}
|
|
|
|
// DataRange returns the minimum and maximum X and Y values
|
|
func (h *Histogram) DataRange() (xmin, xmax, ymin, ymax float64) {
|
|
xmin = math.Inf(+1)
|
|
xmax = math.Inf(-1)
|
|
ymin = math.Inf(+1)
|
|
ymax = math.Inf(-1)
|
|
ylow := math.Inf(+1) // ylow will hold the smallest non-zero y value.
|
|
for _, bin := range h.Bins {
|
|
if bin.Max > xmax {
|
|
xmax = bin.Max
|
|
}
|
|
if bin.Min < xmin {
|
|
xmin = bin.Min
|
|
}
|
|
if bin.Weight > ymax {
|
|
ymax = bin.Weight
|
|
}
|
|
if bin.Weight < ymin {
|
|
ymin = bin.Weight
|
|
}
|
|
if bin.Weight != 0 && bin.Weight < ylow {
|
|
ylow = bin.Weight
|
|
}
|
|
}
|
|
switch h.LogY {
|
|
case true:
|
|
if ymin == 0 && !math.IsInf(ylow, +1) {
|
|
// Reserve a bit of space for the smallest bin to be displayed still.
|
|
ymin = ylow * 0.5
|
|
}
|
|
default:
|
|
ymin = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// Normalize normalizes the histogram so that the
|
|
// total area beneath it sums to a given value.
|
|
func (h *Histogram) Normalize(sum float64) {
|
|
mass := 0.0
|
|
for _, b := range h.Bins {
|
|
mass += b.Weight
|
|
}
|
|
for i := range h.Bins {
|
|
h.Bins[i].Weight *= sum / (h.Width * mass)
|
|
}
|
|
}
|
|
|
|
// Thumbnail draws a rectangle in the given style of the histogram.
|
|
func (h *Histogram) Thumbnail(c *draw.Canvas) {
|
|
ymin := c.Min.Y
|
|
ymax := c.Max.Y
|
|
xmin := c.Min.X
|
|
xmax := c.Max.X
|
|
|
|
pts := []vg.Point{
|
|
{X: xmin, Y: ymin},
|
|
{X: xmax, Y: ymin},
|
|
{X: xmax, Y: ymax},
|
|
{X: xmin, Y: ymax},
|
|
}
|
|
if h.FillColor != nil {
|
|
c.FillPolygon(h.FillColor, c.ClipPolygonXY(pts))
|
|
}
|
|
pts = append(pts, vg.Point{X: xmin, Y: ymin})
|
|
c.StrokeLines(h.LineStyle, c.ClipLinesXY(pts)...)
|
|
}
|
|
|
|
// binPoints returns a slice containing the
|
|
// given number of bins, and the width of
|
|
// each bin.
|
|
//
|
|
// If the given number of bins is not positive
|
|
// then a reasonable default is used. The
|
|
// default is the square root of the sum of
|
|
// the y values.
|
|
func binPoints(xys XYer, n int) (bins []HistogramBin, width float64) {
|
|
xmin, xmax := Range(XValues{xys})
|
|
if n <= 0 {
|
|
m := 0.0
|
|
for i := 0; i < xys.Len(); i++ {
|
|
_, y := xys.XY(i)
|
|
m += math.Max(y, 1.0)
|
|
}
|
|
n = int(math.Ceil(math.Sqrt(m)))
|
|
}
|
|
if n < 1 || xmax <= xmin {
|
|
n = 1
|
|
}
|
|
|
|
bins = make([]HistogramBin, n)
|
|
|
|
w := (xmax - xmin) / float64(n)
|
|
if w == 0 {
|
|
w = 1
|
|
}
|
|
for i := range bins {
|
|
bins[i].Min = xmin + float64(i)*w
|
|
bins[i].Max = xmin + float64(i+1)*w
|
|
}
|
|
|
|
for i := 0; i < xys.Len(); i++ {
|
|
x, y := xys.XY(i)
|
|
bin := int((x - xmin) / w)
|
|
if x == xmax {
|
|
bin = n - 1
|
|
}
|
|
if bin < 0 || bin >= n {
|
|
panic(fmt.Sprintf("%g, xmin=%g, xmax=%g, w=%g, bin=%d, n=%d\n",
|
|
x, xmin, xmax, w, bin, n))
|
|
}
|
|
bins[bin].Weight += y
|
|
}
|
|
return bins, w
|
|
}
|
|
|
|
// A HistogramBin approximates the number of values
|
|
// within a range by a single number (the weight).
|
|
type HistogramBin struct {
|
|
Min, Max float64
|
|
Weight float64
|
|
}
|