render

package
v1.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: MIT Imports: 10 Imported by: 0

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

func Draw(b ir.Backend, c Chart) error

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/refract/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 added in v1.7.0

type ColorbarEntry interface {
	// ColorbarEntry reports a colourbar, or one band of a classed one.
	//
	// cs 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.
	//
	// class is the band's index and lo and hi its interval; class is -1 for a
	// continuous bar, and lo and hi are then the scale's whole domain.
	ColorbarEntry(cs scale.ColorScale, class int, lo, hi float64, area ir.Rect)
}

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 rather than a method on Observer for the reason LayerAxes, EndData and LegendEntry are: Observer never gains one.

type EndData added in v1.7.0

type EndData interface {
	// EndData reports that the data pass is over and everything 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.
	EndData()
}

EndData is an optional interface beside Observer: an observer that implements it is told when the last layer has been drawn.

It exists because Observer has no way to close a layer. Layer opens one and the next Panel opens another, so after the final layer of the final panel the most recent Layer call is still the most recent thing an observer was told — and everything drawn afterwards is attributed to it. What is drawn afterwards is the guides and the chart's Overlay, neither of which is a mark: a pointer landing on a legend swatch has not landed on a row of the layer that happened to be drawn last, and one landing on a crosshair has not landed on anything at all.

It is optional rather than a third method on Observer for the reason LayerAxes is: Observer is implemented outside this package and never gains one ([CONCEPT §15](../CONCEPT.md#15-versioning--stability)).

type LayerAxes added in v1.3.0

type LayerAxes interface {
	// LayerAxes names the scales the next layer opened by [Observer.Layer] is
	// drawn against. It is called immediately before it, and both are ranged
	// for the panel already announced.
	LayerAxes(x, y scale.Scale)
}

LayerAxes is an optional interface beside Observer: an observer that implements it is told which scales the layer about to be opened reads.

It exists because those are not always the panel's own. A layer bound to a secondary axis with github.com/timzifer/refract/geom.OnY2 or OnX2 is drawn against a different scale, and an index that inverted its marks through the panel's own would report a value from the wrong axis — a tooltip naming 4 200 on a chart whose right axis reads 12 %.

It is optional rather than a third method on Observer because Observer is implemented outside this package and never gains one ([CONCEPT §15](../CONCEPT.md#15-versioning--stability)). An observer that does not implement it sees exactly what it saw before there were two axes.

type LegendEntry added in v1.7.0

type LegendEntry interface {
	// LegendEntry reports one row of the legend: which layer it stands for,
	// what it is labelled, the rectangle it occupies, and whether that layer
	// is currently hidden.
	//
	// layer is -1 for a row no layer can be attributed to. The rectangle 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.
	LegendEntry(layer int, label string, area ir.Rect, hidden bool)
}

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 third method on Observer for the reason LayerAxes and EndData are: Observer is implemented outside this package and never gains one.

type Observer added in v0.5.0

type Observer interface {
	// Panel opens a panel: its index in the chart, the rectangle it occupies,
	// the scales that place values in it, and the coord that turns a pair of
	// mapped positions into a point there. The scales 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(i int, area ir.Rect, x, y scale.Scale, cd coord.Coord)

	// Layer opens a layer within the panel just announced: its index among
	// that panel's layers, and its legend label if it has one.
	Layer(i int, label string)
}

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 added in v1.7.0

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 added in v1.7.0

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 added in v1.7.0

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 added in v1.7.0

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 added in v1.7.0

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 added in v0.3.0

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/refract/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 SizeKeyEntry added in v1.7.0

type SizeKeyEntry interface {
	// SizeKeyEntry reports one row of a size key: the value its sample is
	// drawn for, how that value is spelled, and the rectangle the row
	// occupies.
	SizeKeyEntry(value float64, label string, area ir.Rect)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL