Documentation
¶
Overview ¶
Package stat aggregates data before it is drawn.
Three families live here and they answer different questions.
The first is reduction: 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. See LTTB, MinMax and Grid.
The second is distribution: the rows are not too many, they are the wrong shape. A histogram, a density estimate, an ECDF and a smoothing fit all replace the observations with a summary of where they are — which is a different reading of the same column rather than a cheaper one. See Bin, KDE, ECDF, Loess and Hex.
The third is neither: a Family is a set of curves given by a *formula* rather than by data — a Nichols chart's closed-loop contours, a Smith chart's constant-VSWR circles — and it reduces nothing because there is nothing to reduce. It is here because it is numbers in and numbers out like the rest, and because what the curves look like is the coordinate stage's business rather than this package's. See Family and docs/adr/0050-locus-annotations.md.
None of the three changes a scale's domain by itself. A geom decides what its axis describes: a decimating layer trains on every row and reduces only when it draws, so the axis reports what the data holds rather than what survived; a histogram trains on the counts, because the counts are what it draws; and a locus trains nothing at all, because it says what the region of the plane means rather than what is in it.
Purity and determinism ¶
Everything here is a pure function of its arguments. Nothing reads a clock, a map iteration order or math/rand: a parallel render has to be byte identical to a serial one (docs/adr/0012-parallel-panels.md), and a reduction that reached for a random number would quietly end that. Every function has a test that runs it twice and compares.
The functions come in pairs: LTTB and AppendLTTB, Bin and AppendBin. The Append forms write into a caller-owned slice, which is how a chart redrawn every frame keeps its per-frame allocations flat.
There is no Stat interface ¶
Deliberately. A stat here is a function a geom calls in its Train, not a stage between the data and the layer that a caller plugs a different one into. Family is not a counter-example: it is a curve with a name, not a stage — nothing is fed through it, and what it answers is geometry rather than a summary of anybody's rows. A pluggable stat would have to know which axis it decides, how the layer treats a missing value and what the theme wants — which is exactly what this package must not know. A caller with a summary of their own writes a geom that calls it (see docs/adr/0028-distribution-stats.md), and the function itself belongs here only if it is numbers in and numbers out.
Index ¶
- Constants
- func AppendDepth(dst []int, parent []int) []int
- func AppendLTTB[F Float](dst []int, x, y []F, threshold int) []int
- func AppendLevels(dst []float64, lo, hi float64, n int) []float64
- func AppendMinMax[F Float](dst []int, x []F, columns int, ys ...[]F) []int
- func AppendPartition(lo, hi []float64, total []float64, parent, depth []int) ([]float64, []float64)
- func AppendRollup(dst []float64, value []float64, parent, depth []int) []float64
- func AppendStackOffsets(dst []float64, mode StackMode, series [][]float64) []float64
- func Depth(parent []int) []int
- func FamilyName(f Family) (string, bool)
- func FreedmanDiaconis(sorted []float64) int
- func LTTB[F Float](x, y []F, threshold int) []int
- func Levels(lo, hi float64, n int) []float64
- func MinMax[F Float](x []F, columns int, ys ...[]F) []int
- func NormalQuantile(p float64) float64
- func Partition(total []float64, parent, depth []int) (lo, hi []float64)
- func Quantile(sorted []float64, p float64) float64
- func Rollup(value []float64, parent, depth []int) []float64
- func Silverman(sd, iqr float64, n int) float64
- func StackOffsets(mode StackMode, series [][]float64) []float64
- func StdDev(vs []float64) float64
- func Sturges(n int) int
- type Bucket
- type Cell
- type Chord
- type Contour
- type Extent
- type Family
- 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 Hex
- func (h *Hex) Add(x, y float64) bool
- func (h *Hex) At(col, row int) uint32
- func (h *Hex) Cell(x, y float64) (col, row int, ok bool)
- func (h *Hex) Cells(dst []Cell) []Cell
- func (h *Hex) Center(col, row int) (x, y float64)
- func (h *Hex) Fraction(count uint32, s Scaling) float64
- func (h *Hex) Reset(radius, x0, y0, x1, y1 float64)
- type Isoline
- type Lattice
- type LatticeFault
- type Point
- func AppendECDF(dst []Point, sorted []float64) []Point
- func AppendKDE(dst []Point, vs []float64, bw, lo, hi float64, n int) []Point
- func AppendLoess(dst []Point, xs, ys []float64, span float64, n int) []Point
- func AppendQQ(dst []Point, sorted []float64, quantile func(float64) float64) []Point
- func ECDF(sorted []float64) []Point
- func KDE(vs []float64, bw, lo, hi float64, n int) []Point
- func Loess(xs, ys []float64, span float64, n int) []Point
- func QQ(sorted []float64, quantile func(float64) float64) []Point
- func Vertices(dst []Point, cx, cy, radius float64) []Point
- type Ribbon
- type Sankey
- type SankeyFlow
- type SankeyNode
- type Scaling
- type Span
- type StackMode
- type Tile
Constants ¶
const DefaultKDEPoints = 128
DefaultKDEPoints is how finely a density is evaluated when the caller names no resolution. It is well above the number of pixels a violin is drawn across, so the curve is limited by the bandwidth rather than by the sampling.
const DefaultLoessPoints = 64
DefaultLoessPoints is how many abscissae a fit is evaluated at when the caller names no resolution.
const DefaultSpan = 0.75
DefaultSpan is the fraction of the data one local fit sees when the caller names none. Three quarters is Cleveland's own default and is the right starting point for a trend line: enough neighbours that the fit is a trend rather than an interpolation, few enough that it still bends.
const NoParent = -1
NoParent is the parent of a root. Any negative index means the same thing; this is the one to write.
const SankeySweeps = 6
SankeySweeps is how many times a Sankey relaxes its node positions.
It is a constant rather than a tolerance, and that is the whole point. A relaxation that ran "until it settled" would make the picture depend on floating-point noise, and a chart whose panels are built on separate goroutines has to be byte-identical to one built serially (docs/adr/0012-parallel-panels.md). Six sweeps is where the arrangement stops visibly improving on the flows this was tested against; a seventh moves nothing a reader can see.
Variables ¶
This section is empty.
Functions ¶
func AppendDepth ¶
AppendDepth is Depth writing into dst, which it truncates and grows as needed.
func AppendLTTB ¶
AppendLTTB is LTTB appending into dst.
func AppendLevels ¶ added in v0.10.0
AppendLevels is Levels writing into a caller-owned slice, which it truncates first. It is what a chart redrawn every frame calls.
func AppendMinMax ¶
AppendMinMax is MinMax appending into dst.
func AppendPartition ¶
AppendPartition is Partition writing into lo and hi, which it truncates and grows as needed.
func AppendRollup ¶
AppendRollup is Rollup writing into dst, which it truncates and grows as needed. It is the form a geom calls, because a chart redrawn every frame should not allocate a total per node per frame.
func AppendStackOffsets ¶
AppendStackOffsets is StackOffsets writing into dst, which it truncates and grows as needed. It is the form a geom calls, because a chart redrawn every frame should not allocate a baseline per frame.
func Depth ¶
Depth returns how far each node is from a root: zero for a root, one more than its parent for everyone else.
parent[i] is the index of i's parent, or NoParent — or any index outside the list, which means the same. A node that cannot be reached from a root is on a cycle, and comes back as -1 rather than as a guess or a panic: a hierarchy with a cycle is not a hierarchy, and the caller is the one that can say so in terms of its own data.
It sweeps one level at a time rather than recursing, so a hierarchy deep enough to blow a stack does not, and it costs one pass per level. That is a bounded traversal rather than an iteration to convergence: it visits every reachable node exactly once and stops when a level adds nobody, which makes it a pure function of its input in the sense ADR 0012 requires.
func FamilyName ¶ added in v0.10.0
FamilyName is the name f is written down under, or ok == false for a family this package did not define.
func FreedmanDiaconis ¶
FreedmanDiaconis returns the bin count the Freedman–Diaconis rule chooses for an ascending column: the interval divided by 2·IQR·n^(-1/3).
It reports 0 when the rule has no answer — fewer than two values, or an interquartile range of zero, which is what a column of mostly one value has. A caller falls back to Sturges there rather than dividing by nothing.
The column must be sorted ascending, for the reason Quantile is: sorting here would mean either mutating the caller's data or allocating a copy of it on every frame.
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 Levels ¶ added in v0.10.0
Levels returns about n contour levels spanning [lo, hi] at a round step.
The step is the rung of the 1, 2, 5 × 10ⁿ ladder nearest in ratio to (hi-lo)/n, and the levels are its multiples strictly inside the range. Strictly, because an isoline at the minimum of the data is a point and one at the maximum is the boundary, and neither draws anything a reader can read.
About n, and not n. A round step a reader can do arithmetic with is worth more than an exact count of awkward ones — a request for seven levels over [0, 1] is answered with nine at a tenth rather than seven at 0.125 — so the count is a hint at the density and the step is the promise.
It is here beside Sturges and FreedmanDiaconis because it is the same kind of rule: how many, chosen from the data and from nothing else.
Why not the tick search ¶
The scale package has an extended-Wilkinson search that chooses far better numbers, and it is the wrong tool. That search optimises a *labelling* — simplicity, coverage, and density against an axis of a given length — so the count it lands on depends on how wide the panel is. A chart whose number of isolines changed when it was resized would be a chart whose reading changed with its size, which is the rule ADR 0011 states for an axis and ADR 0028 restates for anything computed in Train. A level list has no length to be dense against.
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.
func NormalQuantile ¶
NormalQuantile is the inverse standard normal CDF. It returns infinities at 0 and 1, and NaN outside [0,1]. A QQ plot uses interior plotting positions.
func Partition ¶
Partition returns each node's half-open span [lo, hi) of the unit interval: the fraction of the whole hierarchy its subtree occupies, and where that fraction sits.
It is the layout an icicle and a sunburst draw directly — a node's span across, its depth out — and the ranges a treemap hands to Squarify one sibling group at a time. total comes from Rollup.
Siblings are laid out in index order, which is the order their rows appeared in the source table. Children fill their parent's span from its start; a parent that carries a value of its own beyond its children's keeps the remainder, which is what makes an unaccounted-for share visible rather than invisible.
Every span is zero when the totals sum to nothing, rather than a division by zero: a hierarchy of zeroes has no shape, and the caller draws nothing.
func Quantile ¶
Quantile returns the p'th quantile of an ascending slice by linear interpolation between the two closest order statistics.
This is the definition R calls type 7 and NumPy uses by default. Choosing it deliberately matters: the nine standard definitions disagree by a visible amount on the small samples a boxplot or a bandwidth rule is computed from, and a box that does not match the reader's own analysis is worse than no box.
The slice must already be sorted ascending; sorting it here would mean either mutating the caller's column or allocating a copy of it per frame.
func Rollup ¶
Rollup returns each node's total: its own value plus every descendant's.
value[i] is what row i carries on its own, which for the usual hierarchy — where only the leaves are measured — is zero for every internal node. depth comes from Depth; a node it left at -1 is on a cycle and contributes to nothing, including itself.
A negative value is summed as it stands rather than clamped. A tree that mixes signs has no sensible area to draw and the caller is where that is worth saying; silently taking the absolute value would draw a picture the numbers do not support.
func Silverman ¶
Silverman returns the bandwidth Silverman's rule of thumb chooses: 0.9·min(σ, IQR/1.349)·n^(-1/5).
Both spread measures are the caller's to supply, and iqr may be 0 to use the standard deviation alone. That is not a shortcut — computing an interquartile range means sorting, and a chart redrawn every frame sorts into a buffer it keeps rather than into one this package would allocate. A caller with a sorted column passes Quantile of it; one without passes 0 and gets the σ-only form, which is the rule as Silverman first wrote it.
The robust minimum matters on real data: a single outlier inflates σ without moving the IQR, and a bandwidth taken from σ alone then smooths the whole distribution flat because one row was far away.
func StackOffsets ¶
StackOffsets returns the baseline of each column of a stack.
series[g][p] is group g's value at position p; every group must have the same number of positions, and the positions must be in axis order, because StackWiggle reads each column against the one before it. The result has one baseline per position: the first group's segment runs from base[p] to base[p] + series[0][p], the second from there, and so on.
It is a pure function of its input. Nothing here reaches for a map or for math/rand, so a parallel render stays byte-identical to a serial one — see docs/adr/0012-parallel-panels.md.
func StdDev ¶
StdDev returns the sample standard deviation of vs, ignoring NaN and infinities. It is 0 for fewer than two usable values.
The two-pass form is deliberate. The textbook single-pass identity E[x²]-E[x]² cancels catastrophically on a column whose values are large and whose spread is small — a timestamp column in nanoseconds is exactly that — and a bandwidth rule fed a negative variance produces a density of NaN.
func Sturges ¶
Sturges returns the bin count Sturges's rule chooses for n observations: ceil(log2 n) + 1.
It is the default because it needs nothing but the count — no sort, no spread — so a histogram redrawn every frame costs a pass over the column rather than a copy of it. It assumes roughly normal data and under-bins a large or a skewed sample; FreedmanDiaconis is the answer to that, and it is a separate function because it asks the caller for a sorted column.
Types ¶
type Bucket ¶
Bucket is one bin of a histogram: the interval it covers and how many observations fell in it.
The interval is half open, [Lo, Hi), except for the last bucket of a run, which includes its upper bound — otherwise the largest observation in a column would fall in no bucket at all, which is the one value a reader is certain to look for.
func AppendBin ¶
AppendBin is Bin writing into a caller-owned slice. dst is truncated first, so a chart redrawn every frame bins into the same memory.
func Bin ¶
Bin counts vs into n equal-width buckets over [lo, hi].
Pass lo >= hi to take the interval from the data, and n <= 0 to let Sturges choose the count. Values outside the interval are not counted: a histogram over an explicit range is a statement about that range, and silently folding the tails into the end buckets would misreport both.
type Chord ¶
type Chord struct {
// Arcs has one span per node, in node order. A node no edge touches gets an
// empty span rather than being dropped, so a caller can index Arcs by node.
Arcs []Span
// Ribbons has one entry per edge, in the order the edges were given.
Ribbons []Ribbon
// contains filtered or unexported fields
}
Chord lays an edge list out around the unit interval: one arc per node, sized by the traffic through it, and one ribbon per edge joining a slice of one arc to a slice of another.
Wrapped round a circle by a polar coord this is a chord diagram; left straight under a Cartesian one it is an arc diagram, whose edges rise off a rail instead of crossing a disc. The layout does not know which — it works in the unit interval and the coordinate system decides what that looks like.
It is a struct with a Chord.Reset rather than a pair of functions for the reason Sankey and Hex are: the layout keeps a cursor per node, and a chart redrawn every frame should reuse it rather than allocate it again.
Order ¶
Arcs go round in node order and ribbons leave them in edge order, both of which are the order the caller's rows appeared in. Nothing here sorts, and nothing here reads a map: the picture is a pure function of the table (docs/adr/0012-parallel-panels.md). A caller that wants the busiest node first sorts its own rows.
The zero Chord is unusable; call Chord.Reset first.
func (*Chord) Reset ¶
Reset lays out the edge list from[i] → to[i] carrying value[i], over the given number of nodes, leaving pad of the unit interval between adjacent arcs.
An edge naming a node outside [0, nodes), or carrying a value that is not positive, takes no room and gets an empty ribbon. A self-edge is not a special case: it takes a slice of its node's arc at each end, which draws as a loop.
type Contour ¶ added in v0.10.0
type Contour struct {
// Lines has one run per connected piece of an isoline: grouped by level in
// ascending order, and within a level in the order the runs were found,
// which is lattice order. Nothing here depends on a map's iteration order —
// see the package documentation on determinism.
Lines []Isoline
// Points is the vertex arena every run indexes, in the coordinates the
// lattice was given. A geom maps them through its scales exactly as it maps
// a row.
Points []Point
// Levels is the list the runs were traced at, ascending and with any
// repeats removed.
Levels []float64
// contains filtered or unexported fields
}
Contour traces the isolines of a value sampled on a regular lattice, by marching squares.
It is one tracing serving two charts. A flat contour plot strokes what it returns through the panel's scales; a projected scene strokes the same runs on the floor of its cube. Handed one level list they are the same lines, and that is the point: a reading taken off the plan and a reading taken off the floor of the surface have to agree, and two implementations of marching squares would agree until the first saddle.
It is a struct with a Contour.Reset rather than a pair of functions because its working state is the size of the data, which is Hex's reason and Sankey's. It is not generic over float32, and that is a decision rather than an omission: a reduction is generic so that a geom can run it on projected device coordinates, and a contour runs in Train, in data space, where there is no float32 caller.
The zero Contour is unusable; call Contour.Reset first. Reset keeps the arena and the working tables.
func (*Contour) Line ¶ added in v0.10.0
Line returns the points of one run, as a slice of the arena. It is lent rather than given: the next Contour.Reset refills it.
func (*Contour) Reset ¶ added in v0.10.0
Reset traces z over the lattice xs × ys at the given levels.
z holds len(ys) rows of len(xs) values, row-major in y — which is Lattice.V's layout, and Lattice is how a long table becomes one.
A cell with any non-finite corner is skipped whole. A contour drawn through a hole would be a contour through a number nobody measured, so a run that reaches one ends there rather than being routed round it. A level outside the data's range traces nothing, which is not an error: it is what "show me the 0 dB line" means when nothing reaches 0 dB.
type Extent ¶ added in v0.10.0
Extent is the region of data space a curve is drawn over, and how finely.
The bounds are the panel's own domains, ascending. They are what a repeating family needs in order to know how many times to repeat, and what an unbounded one needs in order to know where to stop; a family whose curves are bounded — a VSWR circle is a circle whatever the axes say — ignores them. Steps is how many samples the panel is wide enough to be worth, which is a question about the screen and is therefore answered when the chart is drawn rather than when its axes are trained.
An empty extent is legitimate: it is what a family is handed before the domains exist, and every family answers it with a curve rather than with nothing.
type Family ¶ added in v0.10.0
Family is one indexed set of curves in data space.
Given a level and the panel's extent, Locus appends the data-space points of that level's curve to xs and ys and returns the extended slices. Both are appended to in lockstep and come back the same length; the caller owns them and truncates them, which is what keeps a chart redrawn every frame from allocating a curve every frame.
A curve that leaves the extent and comes back, or that repeats — a Nichols family repeats every 360° of phase — appends NaN between its runs, which is the gap every mark in figure already understands.
Stability ¶
Family is implemented outside this module, so it never gains a method, and the extent it is handed is a struct for the same reason — ADR 0060. The four families below are values rather than functions so that each has a name that can be written down: FamilyName reports it, FamilyNamed reads it back, and a family a caller wrote in Go has neither and does not serialise.
var ( // NicholsM is the locus of constant closed-loop magnitude |T|, and its // level is that magnitude in decibels. It is what the resonance peak M_r is // read off. The 0 dB member is the perpendicular bisector of the origin and // the critical point rather than a circle, which is the one degenerate case // in either Nichols family. NicholsM Family = nameNicholsM // NicholsN is the locus of constant closed-loop phase ∠T, and its level is // that phase in degrees. Every member passes through L = 0 and through // L = −1, which is why the contours all converge on the critical point and // why each one plunges to −∞ dB where it passes through the origin — a // place no scale can put, and the missing-value policy has covered that // since v0.1. The 0° and 180° members are the real axis itself and are not // drawn. NicholsN Family = nameNicholsN // SmithVSWR is the locus of constant reflection magnitude, and its level is // the standing-wave ratio the chart is read in: 1.5, 2, 3. A level of 1 is a // matched load, which is the middle of the chart and a point rather than a // curve, so it draws nothing. SmithVSWR Family = nameSmithVSWR // SmithQ is the locus of constant reactance-to-resistance ratio |x| = Q·r, // and its level is Q. It is two straight rays in impedance, so it is two // arcs on the disc, by the coord's own map and with no second // implementation. They run to an infinite impedance, which is the rim. SmithQ Family = nameSmithQ )
The built-in families.
The two Nichols families describe the closed loop T = L/(1+L) over a chart whose axes hold the open loop L: X is arg L in degrees and Y is 20·log₁₀|L| in decibels. Both are circles in the complex L-plane before the chart's log-polar step, which is why neither costs more than a few lines of arithmetic.
The two Smith families describe a normalised impedance z = r + jx over a chart whose axes hold r and x, which is the pair github.com/timzifer/figure/coord.Smith reads. Neither of them is drawn as a circle on the disc: each is the set of impedances satisfying a condition, and the coord maps it into the shape it looks like.
func FamilyNamed ¶ added in v0.10.0
FamilyNamed is the family written down under name, or ok == false for a name this package has no family for.
The set is closed on purpose. A family a caller wrote in Go is a perfectly good Family and has no name here, because a name that resolved to something this package cannot rebuild would make a document that does not round-trip — which is ADR 0041's rule for a quantile function, applied to a curve.
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 BinGrid ¶
BinGrid 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.
It is BinGrid rather than Bin because Bin is the one-dimensional histogram, which is what "bin" means without a qualifier. The three binners are named after what they fill: BinGrid, BinHex and Bin itself.
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.
type Hex ¶
type Hex struct {
// Radius is a cell's circumradius: the distance from its centre to a
// vertex, and half its height.
Radius float64
// Cols and Rows are the lattice's shape.
Cols, Rows int
// X0 and Y0 are the centre of cell (0, 0).
X0, Y0 float64
// MinX, MinY, MaxX and MaxY are the rectangle the lattice covers,
// normalised so that the minimum is the lower corner on both axes. A
// device-space Y axis runs downwards and a caller may hand the two corners
// over in either order; a hexagonal lattice is symmetric about both, so
// which way round it was given changes nothing but which points are inside.
MinX, MinY, MaxX, MaxY 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
}
Hex is a hexagonal lattice over a rectangle: how many rows fall in each hexagonal cell.
It is Grid with a better tiling. A square grid has four neighbours at one distance and four at another, so a cloud binned into it grows faint crosses and diagonal seams that are artefacts of the bins rather than features of the data; a hexagon has six neighbours all the same distance away, which is why this is the aggregate a hexbin chart is made of. The other difference is what is drawn: a density raster paints one pixel per cell, and a hexbin draws a mark per cell, so the cells are counted in the hundreds rather than in the hundreds of thousands.
The lattice is pointy-topped: a cell is 2·Radius tall and √3·Radius wide, rows are 1.5·Radius apart, and odd rows are offset half a cell to the right.
The zero Hex is unusable; call Hex.Reset first. Reset keeps the count buffer, so a chart redrawn every frame bins into the same memory.
func BinHex ¶
BinHex counts every row of xs against ys into h, returning it.
The coordinates are whatever h's rectangle is in — device space for a geom binning what it was about to draw, which is what makes the cells regular hexagons on screen rather than hexagons stretched by the axes.
func (*Hex) Cell ¶
Cell returns the cell a position falls in, and whether it is inside the lattice.
The nearest centre is found the way d3-hexbin finds it: round to the nearest row, round to the nearest column of that row, and then — only when the point is in the band where two rows' cells interlock — compare the candidate against the one diagonally across from it. That band is the middle third of a row's height, which is where |py - pj|·3 > 1.
The comparison is not d3's, and the difference is load-bearing. Both work in lattice coordinates, where a column step is 1 and a row step is 1 — but a column step is √3·Radius on screen and a row step is 1.5·Radius, so comparing px² + py² there measures a distance in two different units and hands some points to a cell that is not their nearest. The vertical term therefore carries (dy/dx)², which is exactly 3/4, and [TestEveryHexPointLandsInItsNearestCell] is what holds it: without it the picture grows seams along every second row.
func (*Hex) Cells ¶
Cells appends the populated cells to dst, in row-major order, and returns it. dst is truncated first.
Row-major and not "whichever order a map produced them in": a parallel render has to be byte identical to a serial one, so the order marks are emitted in cannot depend on hashing. See docs/adr/0012-parallel-panels.md.
func (*Hex) Fraction ¶
Fraction maps a count onto [0, 1] against the busiest cell, exactly as Grid.Fraction does.
func (*Hex) Reset ¶
Reset prepares h for cells of the given radius over the rectangle with corners (x0, y0) and (x1, y1), clearing any previous counts and reusing the existing buffer when it is large enough.
A radius of zero or less is meaningless and is replaced with one, which draws a lattice of unit cells rather than dividing by nothing.
type Isoline ¶ added in v0.10.0
type Isoline struct {
// Level is the value the run traces, and Index its position in
// [Contour.Levels] — so a caller colouring by level does not have to search
// for it.
Level float64
Index int
// Lo and Hi bound the run in [Contour.Points]. Reach for [Contour.Line].
Lo, Hi int
// Closed reports a ring: the run's last point is exactly its first. An open
// run begins and ends on the edge of the lattice or at a hole, so this is a
// statement about the points rather than about where the run stopped.
Closed bool
}
Isoline is one connected run of one contour level.
It is a range into the Contour's point arena rather than a slice of its own, so a chart redrawn every frame refills one buffer instead of allocating a slice per line. Sankey.Flows has the same shape for the same reason.
type Lattice ¶ added in v0.10.0
type Lattice struct {
// Xs and Ys are the sorted distinct values of the two axes.
Xs, Ys []float64
// V holds len(Ys) rows of len(Xs) values, row-major in y: the value at
// (Xs[i], Ys[j]) is V[j*len(Xs)+i], which is [Lattice.Index].
V []float64
// Row is the source row behind each cell, in the same order, or -1 for a
// cell no row reached — which happens only when Fault is not [LatticeOK].
Row []int32
// Fault says whether the table was a lattice, and At and With name the rows
// a [LatticeOffGrid] or [LatticeDuplicate] is about so that a caller can
// point its message at the data.
Fault LatticeFault
At, With int
// contains filtered or unexported fields
}
Lattice turns a long table of (x, y, v) rows into a regular grid.
It is the shape a surface, a contour and a raster of a measured field all need, and it is one implementation rather than one each because two resolvers that agree today disagree at the first duplicated position — and the symptom of that is a surface and its own contours that do not line up.
The grid is required rather than guessed. The rows must be the full product of the distinct x and y values with each cell present exactly once, and anything else is a LatticeFault rather than a picture with holes in it: a field drawn over a scattered sample is a field over a triangulation nobody asked for. A cell whose value is NaN is a different matter and is allowed — that is a hole in the data, and a mark decides for itself what to do with one.
The zero Lattice is unusable; call Lattice.Reset first. Reset keeps the buffers and clears the index maps rather than replacing them, so their buckets survive too — which is what keeps a chart redrawn every frame from allocating per frame.
func (*Lattice) Index ¶ added in v0.10.0
Index is where the cell at (Xs[i], Ys[j]) is in Lattice.V and Lattice.Row.
func (*Lattice) Reset ¶ added in v0.10.0
func (l *Lattice) Reset(xs, ys, vs []float64) LatticeFault
Reset fills the lattice from three parallel columns and reports whether they were one.
On a fault the fields are left in whatever state the search reached and must not be read: a caller checks the return, or Lattice.Fault, first.
type LatticeFault ¶ added in v0.10.0
type LatticeFault uint8
LatticeFault says why a table is not a product lattice.
It is a code rather than an error because a useful message names a column and a mark, and this package knows about numbers and nothing else. The caller spells the sentence; this says which one. Sankey.Cyclic is the same trade.
const ( // LatticeOK is a table that is a lattice. LatticeOK LatticeFault = iota // LatticeRagged is three columns of different lengths. LatticeRagged // LatticeTooSmall is fewer than two distinct values on one of the axes. // One value is a line rather than a grid, and no surface, contour or image // can be drawn over it. LatticeTooSmall // LatticeWrongCount is a table with a number of rows that is not the // product of the two axes' distinct values, which means cells are missing // or repeated whatever the positions say. LatticeWrongCount // LatticeBadPosition is a row whose x or y is not a finite number. // [Lattice.At] is the row. // // It is checked rather than discovered, because the axes are the distinct // values of the columns themselves — so every row is at a node of the // lattice by construction, and the only position that is not one is a // position that is not a number. LatticeBadPosition // LatticeDuplicate is two rows at one node. [Lattice.At] and [Lattice.With] // are the two. LatticeDuplicate )
The faults, in the order Lattice.Reset can find them.
type Point ¶
type Point struct{ X, Y float64 }
Point is a position a distribution stat computed: a bin's centre and its count, a density's argument and its value, a fit's abscissa and its estimate.
It is float64 and not ir.Point because none of it is a device coordinate — stat knows about numbers and nothing else, and a density evaluated in float32 would lose the tail of a narrow kernel.
func AppendECDF ¶
AppendECDF is ECDF writing into a caller-owned slice. dst is truncated first.
One point per *distinct* value, not per row: a column with a thousand copies of one number is one step of height 1000/n, and emitting a thousand points at one X would draw a thousand identical vertices and report a thousand marks to a hit test.
func AppendKDE ¶
AppendKDE is KDE writing into a caller-owned slice. dst is truncated first, so a chart redrawn every frame estimates into the same memory.
The cost is one exponential per observation per evaluation point. That is fine for the columns a violin or a ridgeline is drawn from — a group of a few thousand rows over a hundred points — and it is why nothing here reaches for a density estimate on a million-row column, where Grid is the honest answer.
func AppendLoess ¶
AppendLoess is Loess writing into a caller-owned slice. dst is truncated first, so a chart redrawn every frame fits into the same memory.
The window is walked forward rather than searched for: the abscissae ascend, so the neighbourhood of the next one starts at or after this one's. That is what keeps a fit over a long column linear in it rather than quadratic.
func AppendQQ ¶
AppendQQ is QQ reusing dst, which is truncated before appending. A quantile function returning a non-finite value omits that point without changing the plotting positions of the other observations.
func ECDF ¶
ECDF returns the empirical cumulative distribution of an ascending column: one point per distinct value, whose Y is the fraction of observations at or below it.
It is the distribution plot that invents nothing. A histogram picks bin edges and a density picks a bandwidth, and both choices change the picture; an ECDF has no parameter at all, so two of them drawn together are two datasets compared rather than two smoothing choices compared. That is what makes it worth having beside Bin and KDE rather than instead of either.
The column must be sorted ascending, for the reason Quantile is: sorting it here would mean either mutating the caller's data or allocating a copy of it on every frame. NaN and infinities are ignored, which is why the fractions are taken against the number of usable values rather than against len.
func KDE ¶
KDE evaluates a Gaussian kernel density estimate of vs at n equally spaced points across [lo, hi].
A histogram answers "how many are here" and depends on where the bin edges happen to fall; a density estimate answers "how thick is the data here" and does not. That is the whole reason a violin is a violin rather than two histograms back to back: the shape must not change when the first bin edge moves half a bin.
bw is the kernel bandwidth in the data's own units, and it is the one number that decides what the answer looks like — too small and the estimate is a comb of the observations, too large and every distribution is a hump. Pass bw <= 0 to have Silverman choose it. Pass lo >= hi to take the interval from the data, and n <= 0 for DefaultKDEPoints.
The result is normalised as a density: it integrates to 1 over the real line, not over [lo, hi], so two groups drawn on one axis are comparable in the way their areas are.
func Loess ¶
Loess fits a locally weighted linear regression of ys on xs and evaluates it at n equally spaced points across the data.
It is the trend line that does not assume a shape. A straight fit answers "is this rising", which is a question about the whole column at once; loess answers "what is it doing here", by fitting a line through the neighbours of each abscissa and weighting them by how near they are. The span decides how many neighbours that is, as a fraction of the rows in (0, 1]: small spans follow the data, large ones flatten it, and the choice is the reader's rather than this function's.
What the columns must be ¶
xs ascending, both columns finite, and the same length — the shorter one wins if they are not. That is a narrower contract than the rest of this package takes and it is deliberate: sorting and hole-filling both need a buffer, a caller redrawing a chart every frame already keeps one, and doing it here would allocate a copy of the column per frame to save the caller a loop. See github.com/timzifer/figure/geom.Trend, which is that caller.
Pass span <= 0 for DefaultSpan and n <= 0 for DefaultLoessPoints.
The fit is locally *linear* — degree one, not degree two. A quadratic local fit tracks a peak more faithfully and overshoots at the ends of the data, where a trend line is read most confidently and is least supported; the linear form is the conservative half of that trade.
func QQ ¶
QQ compares ascending observations with a theoretical quantile function. X holds theoretical quantiles and Y holds observations, including ties. Plotting positions are (i+0.5)/n, strictly inside (0,1), so distributions with infinite endpoints still give finite tail positions. NaN and infinities in the sample are ignored. A nil quantile uses the standard normal.
Input must already be sorted. The function never edits the caller's sample.
func Vertices ¶
Vertices writes the six corners of a cell of the given radius, centred on (cx, cy), into dst — which is truncated first and returned.
It is here rather than in a geom because the vertices are the lattice's own geometry: a cell drawn a degree out of phase with the one it was counted in would tile with gaps. The first vertex is the top one, and they run clockwise in a coordinate system whose Y axis points down.
type Ribbon ¶
type Ribbon struct {
Src, Dst Span
}
Ribbon is one edge's two ends: the span it takes on its source's arc and the span it takes on its target's. The two are the same width, because they are the same quantity read twice — which is exactly what a chord diagram asserts and what tells it apart from two bar charts side by side.
type Sankey ¶
type Sankey struct {
// Nodes has one entry per node, indexed as the caller's edge list indexes
// them.
Nodes []SankeyNode
// Flows has one entry per link, in the order the links were given.
Flows []SankeyFlow
// Layers is how many columns the diagram has, and Cyclic reports an edge
// list that is not a DAG — in which case Nodes and Flows are empty, because
// a flow that returns to where it came from has no column to stand in.
Layers int
Cyclic bool
// contains filtered or unexported fields
}
Sankey lays an edge list out as a flow diagram: nodes in columns, links as bands whose thickness is their value.
It is a struct with a Sankey.Reset rather than a pair of functions because the layout has working state the size of the data — a value per node, a cursor per node — and a chart redrawn every frame should reuse it rather than allocate it again. That is the shape Hex has, for the same reason.
What it decides, and what it does not ¶
It decides two things: which column each node stands in, and how far down each node and each band sits. It does **not** decide the order of the nodes within a column — that is the order they were given in, which is the order their rows appeared in the source table. Reordering nodes to reduce crossings would mean a sort per sweep, and a sort is where a layout stops being a pure function of its input and starts depending on how a tie was broken. A caller that wants a different order sorts its own rows and hands them over that way.
The zero Sankey is unusable; call Sankey.Reset first.
func (*Sankey) Reset ¶
Reset lays out the edge list from[i] → to[i] carrying value[i], over the given number of nodes, leaving pad of the unit interval between adjacent nodes in a column.
pad is a fraction rather than a length because everything here is in the unit square: the geom knows how wide the panel is and this does not.
An edge naming a node outside [0, nodes) is skipped, and an edge list with a cycle sets Sankey.Cyclic and lays nothing out.
type SankeyFlow ¶
SankeyFlow is one link's band, given as the span it occupies where it leaves its source and the span it occupies where it enters its target. The two differ, which is what makes the band a ribbon rather than a rectangle.
type SankeyNode ¶
type SankeyNode struct {
// Layer is the node's column, counting from zero at the sources.
Layer int
// Lo and Hi are the node's near and far edge, in [0, 1].
Lo, Hi float64
// Value is the flow through the node: the greater of what enters it and
// what leaves it.
Value float64
}
SankeyNode is one node's place in a flow diagram: which column it stands in, and the span of the unit interval it fills.
type Scaling ¶
type Scaling uint8
Scaling is how a cell's count becomes a fraction of the busiest cell.
type Span ¶
type Span struct {
Lo, Hi float64
}
Span is a half-open interval [Lo, Hi) of the unit interval.
type StackMode ¶
type StackMode uint8
StackMode is where the bottom of a stack sits.
Stacking itself is a running sum and needs no help. What differs between a stacked bar chart, a 100 % chart and a streamgraph is only the *baseline* each column of the stack is measured from, which is what this package computes: numbers in, numbers out, no scales and no geometry.
const ( // StackZero puts the bottom of every stack on zero. It is the ordinary // stacked bar or area. StackZero StackMode = iota // StackFill normalises each column to sum to one, so the stack fills the // axis and the chart reads as proportions. The baseline is still zero; // the caller scales the values. StackFill // StackSilhouette centres each column on zero, which is the symmetric // baseline a ThemeRiver is drawn about. StackSilhouette // StackWiggle minimises the total slope of the interior boundaries, which // is what makes a streamgraph readable: the eye follows a band by its // thickness, and a band that is also climbing steeply is hard to follow. // Byron & Wattenberg, "Stacked Graphs — Geometry & Aesthetics" (2008). StackWiggle )
The stack baselines.
type Tile ¶
type Tile struct {
X0, Y0, X1, Y1 float64
}
Tile is one rectangle of a treemap: the box a value was given.
It is four float64s rather than an ir.Rect because stat does not import ir, and it is a value rather than a pointer because a treemap of a hundred thousand nodes is a slice of these and nothing else.
func AppendSquarify ¶
AppendSquarify is Squarify writing into dst, which it truncates and grows as needed. It is the form a geom calls, because a chart redrawn every frame should not allocate a tile per node per frame.
func Squarify ¶
Squarify packs values into a rectangle, one tile per value, each with an area proportional to its value and an aspect ratio as close to square as the packing allows.
It is the squarified treemap of Bruls, Huizing and van Wijk (2000): tiles are gathered into a row along the rectangle's shorter side for as long as adding one improves the row's worst aspect ratio, then the row is laid out and the rest of the rectangle is packed the same way. The alternative — slicing the rectangle in one direction — produces slivers that a reader cannot compare, which is the whole reason this algorithm exists.
It lays one level out: the values are one node's children, and a hierarchy is drawn by calling it once per sibling group with the box that group's parent was given. Recursing here would make it a tree walker in a package that knows about numbers and nothing else.
Order is the caller's ¶
Values are packed in the order they are given, and that order is what decides which tiles end up beside which. Descending order gives the best aspect ratios and is what a treemap usually wants; source order keeps the picture stable as the numbers move, which is what an animated one wants. This function sorts neither way — sorting would mean mutating the caller's column or allocating a copy of it per frame, and the caller already has a buffer (see CONTRIBUTING, "Take ordered input rather than sorting").
A value that is zero or negative gets an empty tile where it falls, rather than being dropped: the result has one tile per value, at the same index, so a caller can read a tile against the row it came from without a second mapping.