1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
|
// 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 0 != bin.Weight {
ymax = trY(bin.Weight)
}
xmin := trX(bin.Min)
xmax := trX(bin.Max)
pts := []vg.Point{
{xmin, ymin},
{xmax, ymin},
{xmax, ymax},
{xmin, 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{
{xmin, ymin},
{xmax, ymin},
{xmax, ymax},
{xmin, 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
}
|