Files
sjy01-image-proc/vendor/gonum.org/v1/plot/plotter/histogram.go
2024-10-24 15:46:01 +08:00

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
}