Documentation
¶
Overview ¶
Package stat aggregates data before it is drawn.
Everything here answers the same question: a column has more rows than the plot has pixels, so which rows actually decide what the reader sees? The answers differ by mark. A line wants the rows that preserve its shape; a signal envelope wants the extremes of every pixel column, because a spike one sample wide is the reason someone opened the chart; a point cloud wants no rows at all but a count per cell, drawn as an image.
None of it changes a scale's domain. A geom trains on every row and aggregates only when it draws, so an axis reports what the data holds rather than what survived the reduction.
The functions come in pairs: LTTB and AppendLTTB, MinMax and AppendMinMax. The Append forms write into a caller-owned slice, which is how a chart redrawn every frame keeps its per-frame allocations flat.
Index ¶
- func AppendLTTB[F Float](dst []int, x, y []F, threshold int) []int
- func AppendMinMax[F Float](dst []int, x []F, columns int, ys ...[]F) []int
- func LTTB[F Float](x, y []F, threshold int) []int
- func MinMax[F Float](x []F, columns int, ys ...[]F) []int
- type Float
- type Grid
- func (g *Grid) Add(x, y float64) bool
- func (g *Grid) At(col, row int) uint32
- func (g *Grid) Cell(x, y float64) (col, row int, ok bool)
- func (g *Grid) Fraction(count uint32, s Scaling) float64
- func (g *Grid) Raster(dst *image.NRGBA, s Scaling, paint func(t float64) color.NRGBA) *image.NRGBA
- func (g *Grid) Reset(cols, rows int, x0, y0, x1, y1 float64)
- type Scaling
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppendLTTB ¶
AppendLTTB is LTTB appending into dst.
func AppendMinMax ¶
AppendMinMax is MinMax appending into dst.
func LTTB ¶
LTTB reduces a series to at most threshold rows with the largest-triangle-three-buckets algorithm, returning the row numbers it kept in ascending order.
LTTB splits the rows into equal-count buckets and keeps, from each, the row forming the largest triangle with the row kept before it and the mean of the bucket after it. Area is a proxy for "how much of the line's shape this row carries", which is why peaks and inflections survive a reduction that drops nine points in ten while a running average flattens them.
It is lossy, and honestly so: the result is a subset of real rows, never an invented one, so every vertex the reader sees is a measurement that was taken. The first and last rows are always kept.
Rows are assumed to be ordered along x. That is not an extra condition — a line geom already connects consecutive rows, so a series it can draw is a series this can bucket.
func MinMax ¶
MinMax reduces a series to the rows that decide what each column of pixels looks like, returning the row numbers it kept in ascending order.
Each column keeps four rows at most: the one that entered it, the smallest and the largest value in it, and the one that left. Keeping the extremes is what makes this reduction visually lossless for a signal — a spike one sample wide still reaches full height, where LTTB would weigh it against its neighbours and might drop it. Keeping the entry and the exit is what makes the segments between columns land where the data actually crosses them.
ys is every value column the mark occupies: one for a line, two for a band, so that the kept rows bound the whole shape rather than one edge of it.
Rows are assumed to be ordered along x, as in LTTB.
Types ¶
type Float ¶
Float is the coordinate type these functions accept.
Both widths are here because both are real: a geom decimates in device space, where coordinates are float32 and a pixel is the unit that matters, while a caller aggregating before it ever reaches a chart has float64 data. Converting one to the other to cross this boundary would cost a copy of the whole column.
type Grid ¶
type Grid struct {
// Cols and Rows are the grid's shape.
Cols, Rows int
// X0, Y0, X1, Y1 are the region the grid covers. X0 may exceed X1, and Y0
// may exceed Y1: a device-space Y axis runs downwards, and a grid over it
// has to bin the same way round as the axis it covers.
X0, Y0, X1, Y1 float64
// Counts holds Cols*Rows cells in row-major order.
Counts []uint32
// Max is the busiest cell's count, and N the number of rows binned.
Max uint32
N int
}
Grid is a two-dimensional histogram: how many rows fall in each cell of a regular grid over a rectangle.
It is the aggregate behind the density raster. A scatter of ten million points has no honest drawing as ten million markers — they overplot, the last one drawn wins, and the picture says more about row order than about the data. Counting per cell and painting the counts says how many rows are there, which is the thing the marks were standing in for.
The zero Grid is unusable; call Grid.Reset first. Reset keeps the count buffer, so a chart redrawn every frame bins into the same memory.
func Bin ¶
Bin counts every row of xs against ys into g, returning it.
The coordinates are whatever g's rectangle is in — device space for a geom binning what it was about to draw, data space for a caller aggregating before it builds a chart.
func (*Grid) Cell ¶
Cell returns the cell a position falls in, and whether it is inside the grid. A point on the far edge belongs to the last cell rather than to a cell that does not exist.
func (*Grid) Fraction ¶
Fraction maps a count onto [0, 1] against the busiest cell. An empty cell is 0 under every scaling, which is what keeps the background of a density raster empty rather than faintly painted.
func (*Grid) Raster ¶
Raster paints the grid, one pixel per cell, and returns the image.
paint is called once per non-empty cell with that cell's Grid.Fraction; empty cells are left fully transparent so the plot's own background and grid read through. dst is reused when it is large enough, so a chart redrawn every frame paints into the same pixels; pass nil to have one allocated.