Documentation
¶
Overview ¶
Package render lowers a resolved chart into IR.
This is the one place that knows the drawing order of a chart — background, grid, axes, data, guides — and the one place that turns a layout rectangle and a set of scales into actual primitives. Geoms emit their own marks; everything around them is built here.
Who it is for ¶
The root package is the supported way to draw a chart: it resolves a Plot into the Chart this package takes and handles the parts that are plumbing — Chart.Serial, Chart.Observer, Chart.RowSink. This package is public for the caller that has no Plot: a tool that assembles a chart from the model packages directly, or that wraps a backend to watch what a render emits through Observer. Chart and Panel grow by gaining fields, and a zero field always means what it meant.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Draw ¶
Draw lowers c into b. It does not call Flush: the caller owns the backend's lifecycle.
The order is fixed here and nowhere else: background, then every panel's grid and axes, then the titles, then every panel's data inside its own clip, then the guides. Data is drawn after the furniture so that a mark is never hidden by a grid line, and the guides last because they sit outside every panel and must not be clipped by one.
Types ¶
type Chart ¶
type Chart struct {
Width, Height int
DPR float64
Theme theme.Theme
Title string
XTitle string
YTitle string
// Y2Title labels the secondary vertical axis, down the right-hand side,
// and X2Title the secondary horizontal one, along the top.
Y2Title string
X2Title string
X, Y scale.Scale
// Y2 is the chart's secondary vertical axis, drawn down the right-hand
// side, and X2 its secondary horizontal one, drawn along the top. Each is
// read by the layers that asked for it with
// [github.com/timzifer/figure/geom.OnY2] or OnX2, and each is nil for a
// chart with one axis in that direction — which is every chart written
// before there were two.
//
// They are the chart's rather than a panel's for the reason the coord is:
// the panels of a facet are one plot over different rows, and a second
// axis that meant different things in different panels would be a
// different chart. A grid of subplots gives each panel its own through
// [Panel.Y2] and [Panel.X2].
Y2 scale.Scale
X2 scale.Scale
// Coord is the stage between the scales and the IR: what the interval a
// scale maps into means. Nil is [coord.Cartesian], which is the identity,
// so a chart that names no coord draws exactly what it always drew.
//
// It belongs to the chart rather than to a panel: the panels of a facet are
// the same plot over different rows, and one of them in a different
// coordinate system would be a different chart.
Coord coord.Coord
Layers []geom.Geom
// ShowLegend requests a legend. Entries come from the layers.
ShowLegend bool
// Description is what the chart says about itself in words, for a backend
// that can carry it — see [ir.Semantics]. It is announced before anything
// is drawn and never affects what is.
Description ir.Description
// Math typesets the notation in this chart's labels. It is nil for a chart
// whose labels are text, which is the default and costs nothing: the
// backend is wrapped only when there is a typesetter to wrap it with.
Math mathtext.Typesetter
// Panels, Rows and Cols describe a multi-panel chart: subplots, or the
// facets of one plot. When Panels is empty the chart is the single panel
// described by X, Y and Layers.
Panels []Panel
Rows, Cols int
// RowHeights fixes the height of a grid row in device units, leaving the
// solver to size any row whose entry is zero or absent. It is how a track
// is given the height it was asked for, and how a subplot grid is told
// that one of its rows is a strip rather than a panel.
RowHeights []float32
// ColWidths fixes the width of a grid column in device units, leaving the
// solver to size any column whose entry is zero or absent. It is
// RowHeights turned a quarter turn, and it is what a left or right track
// is given its width by.
ColWidths []float32
// Serial draws the panels one at a time. The zero value builds them
// concurrently where that is possible and worth it — see [drawData] — and
// produces the same output either way.
Serial bool
// Observer is told which panel and which layer is drawing, so that a
// caller watching the backend can attribute a mark to the layer that made
// it. It is nil for an ordinary render, and setting it forces the serial
// path: the observer is told things in order, and two panels drawing at
// once have no order to be told in.
Observer Observer
// RowSink, when non-nil, collects which source row is behind each mark. It
// is separate from Observer because it is a separate cost: a layer does
// the bookkeeping only when someone is listening, so this is what turns it
// on. See [geom.Rows].
//
// It is not called Rows because that name is already the facet grid's.
RowSink geom.Rows
// Hidden turns individual layers off by index, without removing them: a
// hidden layer is not drawn, still trains its scales, and still appears in
// the legend — dimmed, so that a reader can see what they have put away
// and bring it back.
//
// It is indexed by the layer's position among the chart's layers, which is
// the index an [Observer] is told and the one [interact.Hit] reports. A
// shorter slice than there are layers hides none of the rest, and nil
// hides nothing at all.
//
// The scales are trained from hidden layers on purpose. A legend toggle is
// a reading aid — let me see this one without that one on top — and an
// axis that moved every time one was clicked would make the two readings
// incomparable, which is the thing the toggle was for. A caller who wants
// the axes to follow what is left removes the layer instead, with
// [Plot.SetLayers], which is a different statement about the chart.
Hidden []bool
// Overlay paints over the finished chart — a crosshair, a tooltip, a brush
// rectangle. It is nil for an ordinary chart and costs nothing then.
//
// It is drawn last, after the guides, and is not announced to Observer:
// what an overlay draws is not a mark and must not be hit-testable. See
// [Overlay].
Overlay Overlay
}
Chart is a fully specified chart, ready to be drawn.
type ColorbarEntry ¶
type ColorbarEntry interface {
// ColorbarEntry reports a colourbar, or one band of a classed one.
ColorbarEntry(e ColorbarInfo)
}
ColorbarEntry is an optional interface beside Observer: an observer that implements it is told where a colourbar was drawn.
A classed colourbar reports one call per band, because a band is a discrete thing a reader can mean — the rows between these two numbers. A continuous one reports a single call for the whole bar, because every point of it means something different and there is nothing discrete to enumerate.
It is optional for the reason LegendEntry is.
type ColorbarInfo ¶
type ColorbarInfo struct {
// Scale is the scale the bar was painted from, so that a caller can ask
// what value the ramp reaches at a point of it — which is the ramp's
// answer rather than the axis's, and the two disagree wherever the ramp is
// compressed.
Scale scale.ColorScale
// Class is the band's index, or -1 for a continuous bar.
Class int
// Lo and Hi are the band's interval, or the scale's whole domain for a
// continuous bar.
Lo, Hi float64
// Area is the rectangle the bar or band occupies.
Area ir.Rect
}
ColorbarInfo is a colourbar, or one band of a classed one, as ColorbarEntry is told about it.
type DepthObserver ¶ added in v0.10.0
type DepthObserver interface {
// Depth is how far the marks that follow are from the camera.
Depth(d float64)
}
DepthObserver is implemented by an Observer that wants to know how far from the camera each mark it is about to be told about was drawn.
It is optional, and it is optional because most charts have no answer: a flat chart has no depth, and nothing in this package calls it. figure/three does, once per primitive, with the number its painter sorted by — larger is farther.
It exists because a projected scene hides its own far side, and an observer that indexes marks cannot otherwise tell a mark in front from the one behind it: the topmost mark at a point is the nearest, but nothing says how near, so nothing can say whether some *other* mark the caller is asking about is behind it. A host ringing the row a reader picked needs exactly that, so that a ring over a point on the far side of a surface can say so rather than claiming the point is where the near face is.
The depth holds until it is set again and is reset for each layer, which is the contract Observer.Layer already has for everything else.
type LayerInfo ¶
type LayerInfo struct {
// Index is the layer's position among its panel's layers, in paint order.
Index int
// Label is the layer's legend label, empty if it has none.
Label string
// X and Y are the scales this layer is drawn against, ranged for the
// panel already announced. They are the panel's own unless the layer is
// bound to a secondary axis.
X, Y scale.Scale
}
LayerInfo is what Observer.Layer is handed: which layer is opening, and what it is drawn against.
The scales are here rather than in a second call because they are not always the panel's own: a layer bound to a secondary axis with github.com/timzifer/figure/geom.OnY2 or OnX2 is drawn against a different one, and an index that inverted its marks through the panel's would report a value from the wrong axis — a tooltip naming 4 200 on a chart whose right axis reads 12 %. It is a field rather than a second call beside Layer, and the next such fact is another field. ADR 0060 is the record.
type LegendEntry ¶
type LegendEntry interface {
// LegendEntry reports one row of the legend.
LegendEntry(e LegendInfo)
}
LegendEntry is an optional interface beside Observer: an observer that implements it is told where each row of the legend was drawn.
It is what makes a legend answer to a pointer. A legend is furniture — it is drawn after the data and is not a mark — but it is the one piece of furniture a reader expects to be able to *act on*, by clicking a series to put it away. So it is announced separately from the marks and with its own vocabulary, rather than being indexed as though it were data: a hit on a swatch has to be distinguishable from a hit on the thing the swatch stands for, or a tooltip would describe a row that is not under the pointer.
It is optional rather than a method on Observer because an observer that does not care about guides is not wrong, only narrower — which is what separates it from Observer.End.
type LegendInfo ¶
type LegendInfo struct {
// Layer is the layer the row stands for, or -1 for a row no layer can be
// attributed to.
Layer int
// Label is what the row is labelled.
Label string
// Area is the rectangle the row occupies. It spans the legend's width, so
// the gap between a swatch and its label is part of the same target — a
// reader aiming at a word should not have to hit the word.
Area ir.Rect
// Hidden reports whether the layer the row stands for is currently
// hidden.
Hidden bool
}
LegendInfo is one row of the legend, as LegendEntry is told about it.
type Observer ¶
type Observer interface {
// Panel opens a panel. The scales it carries are ranged for this panel and
// must not be modified; the coord is framed for it and is what turns a
// device position back into a pair — which is the only way a tooltip over
// a pie slice names a value rather than a pixel.
Panel(p PanelInfo)
// Layer opens a layer within the panel just announced.
Layer(l LayerInfo)
// End reports that the data pass is over and everything drawn after it is
// furniture. It is called once per render, after the last layer of the
// last panel, and is not called at all by a render with no layers.
//
// It is a method rather than the optional interface it used to be because
// an observer that does not close its last layer is wrong rather than
// merely less capable: Layer opens a layer and the next Panel opens
// another, so without End everything drawn after the final layer — the
// guides, the chart's [Overlay] — is attributed to it. A pointer landing
// on a legend swatch has not landed on a row of whichever layer happened
// to be drawn last, and one landing on a crosshair has not landed on
// anything at all. An observer with nothing to close implements it empty.
End()
}
Observer is told the structure a render is drawing, as it draws it.
It is how hit-testing gets built without widening the IR. A backend sees primitives — a polyline, some markers — and nothing about which layer of which panel emitted them; an Observer is told that separately, so a caller wrapping the backend can tag what it sees. Nothing here draws, and nothing here can change what is drawn.
The calls come in paint order: one Panel, then a Layer for each of its layers, then the next Panel. Only the data pass is announced — the grid, the axes and the guides are furniture, and a pointer landing on a grid line has not landed on anything.
An Observer is implemented outside this package, so it never gains a method.
type Overlay ¶
type Overlay interface {
// DrawOverlay paints over the finished chart.
DrawOverlay(b ir.Backend, f OverlayFrame)
}
Overlay paints over a finished chart: a crosshair, a tooltip, a brush rectangle, a ring around the mark another chart is pointing at.
Why it is not a layer ¶
A layer's positions come from its data, through the scales. An overlay's come from somewhere a layer has no access to — a pointer, a selection, or where the *previous* frame put something — and a layer that read any of those would be a layer whose data depended on its own last drawing. So this is a separate stage with a separate seam, drawn after everything else.
It is also not announced to the Observer, and therefore not hit-testable. That is deliberate and it is the property that makes an overlay usable: a tooltip a pointer can hit is a tooltip that flickers, because hovering it moves the pointer off whatever the tooltip was about.
What it is given ¶
The backend, and where the panels are. It is not clipped: a crosshair wants to be confined to its panel and a tooltip wants to overflow one, so an overlay that wants a clip pushes its own — OverlayPanel.Area is what to push. Drawing outside the canvas is the caller's business, as it is for a geom.
Cost ¶
A chart with no overlay pays nothing: Chart.Overlay is nil and this is not called. What an overlay costs when there is one is the calls it makes, plus one consideration that is easy to miss — an overlay that *appears* or *disappears* changes how many calls a frame has, which makes that frame not comparable with the last and therefore a full repaint. One that only moves is a damage rectangle like anything else. A crosshair following a pointer is the cheap case; one that blinks on and off at every panel boundary is two full repaints per crossing.
An Overlay is implemented outside this package, so it never gains a method.
type OverlayFrame ¶
type OverlayFrame struct {
// Canvas is the whole drawing, in device space.
Canvas ir.Rect
// Panels are the chart's panels, in chart order.
Panels []OverlayPanel
// Theme is the chart's theme, so that an overlay drawn over a dark chart
// is legible on it.
Theme theme.Theme
}
OverlayFrame is what an overlay is told about the chart it is drawing over.
func (OverlayFrame) PanelAt ¶
func (f OverlayFrame) PanelAt(pt ir.Point) (OverlayPanel, bool)
PanelAt reports which panel contains a device point, for an overlay deciding which one a pointer is in.
type OverlayPanel ¶
type OverlayPanel struct {
// Index is the panel's position in the chart, matching the index an
// [Observer] was told and the one [interact.Hit] reports.
Index int
// Area is the panel's plot rectangle in device space.
Area ir.Rect
// X and Y are the panel's scales, ranged to Area. An overlay reads them
// to turn a value into a place — a crosshair at a threshold, a band
// between two dates — and must not modify them.
X, Y scale.Scale
// Y2 and X2 are the panel's secondary axes, or nil where it has none.
Y2, X2 scale.Scale
// Coord is the coordinate system the panel was drawn in, framed to Area.
// It is what turns a mapped pair into a point, and is [coord.Cartesian]
// for a chart that named no coord — reach for [OverlayPanel.Coords].
Coord coord.Coord
}
OverlayPanel is one panel of the frame an overlay is drawing over: where it is, and what places values in it.
func (OverlayPanel) Coords ¶
func (p OverlayPanel) Coords() coord.Coord
Coords is the panel's coordinate system, which is coord.Cartesian framed in the plot rectangle when it has none.
type Panel ¶
type Panel struct {
// Row and Col place the panel in the grid.
Row, Col int
// Strip and RightStrip are the labels naming the panel, above it and
// beside it.
Strip, RightStrip string
// X and Y are this panel's scales. Panels sharing an axis share the scale
// object, which is what makes the axis shared rather than merely similar.
X, Y scale.Scale
// Y2 and X2 are this panel's secondary vertical and horizontal axes, or
// nil for a panel with one in that direction. The layers that read them
// are the ones that answer
// [github.com/timzifer/figure/geom.OnSecondaryY] and OnSecondaryX.
Y2 scale.Scale
X2 scale.Scale
// Coord overrides the chart's coordinate system for this panel, and is nil
// for the panels that use it — which is every panel of a facet, because
// the panels of a facet are one plot over different rows and one of them
// in a different coordinate system would be a different chart.
//
// A grid of subplots is the case that needs it: those panels are separate
// plots that happen to share a canvas, each with its own scales and its
// own axes, so a pie beside a bar chart is two coords beside each other.
Coord coord.Coord
// Layers are this panel's marks.
Layers []geom.Geom
// ShowX and ShowY report whether this panel writes its own tick labels. A
// panel that shares an axis with its neighbour leaves the labels to the
// edge of the grid. ShowY2 and ShowX2 are the same question for the
// secondary axes, answered at the right-hand and top edges of the grid
// rather than the left and bottom ones.
ShowX, ShowY, ShowY2, ShowX2 bool
// HideGrid suppresses this panel's grid lines while leaving its fill, its
// axes and its tick marks alone. A track — a band on the panel's own X,
// whose rows are lanes rather than a quantity — is what it is for: a grid
// line through a gantt strip is a rule drawn across a solid bar.
HideGrid bool
}
Panel is one Cartesian area of a multi-panel chart.
type PanelInfo ¶
type PanelInfo struct {
// Index is the panel's position in the chart, in paint order.
Index int
// Area is the rectangle the panel occupies in device space.
Area ir.Rect
// X and Y are the panel's scales, ranged for it.
//
// Both are nil for a panel that has no screen axes to invert through: a
// projected scene is announced by [github.com/timzifer/figure/three] with
// a Z and neither of these, because a device point in a turned cube does
// not resolve to a pair of values and saying it did would be worse than
// saying nothing. A reader of this struct checks before inverting, as
// interact.Index does.
X, Y scale.Scale
// Z is the panel's depth scale, and is nil for the two-dimensional panels
// this package draws. ADR 0056 is the record.
Z scale.Scale
// Coord turns a pair of mapped positions into a point in the panel, and
// inverts one back into a pair. It is nil for a panel whose positions
// were projected rather than mapped.
Coord coord.Coord
}
PanelInfo is what Observer.Panel is handed: which panel is opening, where it is, and what places values in it.
It is a struct rather than a parameter list because a chart can gain a dimension, and Observer is implemented outside this package and so never gains a method or a parameter. A struct with exported fields gains a field instead, and an observer written against today's fields keeps compiling. ADR 0056 is the record.
type SizeKeyEntry ¶
type SizeKeyEntry interface {
// SizeKeyEntry reports one row of a size key.
SizeKeyEntry(e SizeKeyInfo)
}
SizeKeyEntry is an optional interface beside Observer: an observer that implements it is told where each row of a size key was drawn, and what value the row's sample stands for.
type SizeKeyInfo ¶
type SizeKeyInfo struct {
// Value is the value the row's sample is drawn for.
Value float64
// Label is how that value is spelled.
Label string
// Area is the rectangle the row occupies.
Area ir.Rect
}
SizeKeyInfo is one row of a size key, as SizeKeyEntry is told about it.