Documentation
¶
Overview ¶
Package interact turns a rendered chart into something a pointer can ask questions of.
It has two halves. Index is the spatial index: it watches a render, notes where every data mark landed and which layer of which panel drew it, and answers "what is under this point". Event and its kinds are the vocabulary pointer input is reported in; the root package's Live wires the two together — turning a browser's pointer, wheel and drag into hits, zooms and pans — and redraws.
Why it watches rather than asks ¶
A geom emits primitives and forgets them. Asking a layer afterwards where its rows ended up would mean every geom carrying a second, parallel implementation of its own projection — and the two would disagree, on some axis type, eventually. So the index takes the marks the render actually emitted, which is the definition of what the reader can see, and turns a device position back into data through the same scales that put it there.
What it costs ¶
Only a watched render pays: Index.Watch copies the points a layer draws, which is memory proportional to the marks on screen. That is the reduced count rather than the row count — a line over a million rows draws a couple of thousand marks after decimation — but it is not nothing, which is why an unwatched render still allocates nothing that grows with its data.
Index ¶
- Constants
- type Event
- type EventKind
- type Hit
- type Index
- func (ix *Index) At(pt ir.Point, tol float32) (Hit, bool)
- func (ix *Index) ColorbarEntry(e render.ColorbarInfo)
- func (ix *Index) End()
- func (ix *Index) Layer(l render.LayerInfo)
- func (ix *Index) LegendEntry(e render.LegendInfo)
- func (ix *Index) Locate(panel, layer, row int) (ir.Point, bool)
- func (ix *Index) MarkCount() int
- func (ix *Index) Marks(m geom.MarkRows)
- func (ix *Index) Panel(p render.PanelInfo)
- func (ix *Index) PanelAt(pt ir.Point) (int, bool)
- func (ix *Index) Panels() []Panel
- func (ix *Index) Reset()
- func (ix *Index) RowCount() int
- func (ix *Index) RowsIn(r ir.Rect, dst []RowRef) []RowRef
- func (ix *Index) RowsOf(panel, layer int, dst []RowRef) []RowRef
- func (ix *Index) SizeKeyEntry(e render.SizeKeyInfo)
- func (ix *Index) TrackRows(on bool) *Index
- func (ix *Index) TrackingRows() bool
- func (ix *Index) Watch(b ir.Backend) ir.Backend
- type Kind
- type Panel
- type RowRef
Constants ¶
const DefaultTolerance = 12
DefaultTolerance is how near a pointer has to be to a mark to hit it, in device units.
Twelve pixels is about a fingertip and about the radius within which a reader believes they are pointing at a thing. A smaller number makes a thin line unhittable; a much larger one makes two adjacent series indistinguishable.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Event ¶
type Event struct {
// Kind is what happened. Every event has one.
Kind EventKind
// Point is where the pointer was, in device space. Hover, Click, Zoom and
// Pan all have one; Leave carries the last position.
Point ir.Point
// Panel is which panel Point is in, or -1 for a point outside every panel.
Panel int
// Hit is the mark under the pointer, and Found whether there was one.
// They are set on Hover and Click.
Hit Hit
Found bool
// Key identifies the row under the pointer: the value its layer's key
// column holds at [Hit.Row], from
// [github.com/timzifer/figure/geom.KeyBy].
//
// It is what a caller linking two charts sends across. A row number is an
// index into a table as it stands this frame, so it names a different
// measurement after an append, a filter or a window; a key is a value the
// data carries, so it survives all three and means the same thing in
// another chart drawn from another table.
//
// It is empty when the layer named no key column, when row tracking is off,
// and when the mark has no row behind it. Those are three different reasons
// for the same answer, and [Hit.Row] tells them apart when it matters.
//
// It is on the event rather than on [Hit] because reading it means reading
// the data, and this package does not: an Index knows where marks are and
// which layer drew them, not what the layer holds. The root package fills
// it in, where the plot's layers and their sources are both in scope.
Key string
// Rect is the region a rubber-band zoom selected, in device space. It is
// set on Zoom when the zoom came from a selection rather than a wheel;
// [ir.Rect.Empty] reports which.
Rect ir.Rect
// Factor is a wheel zoom's scale factor: below 1 zooms in, above 1 zooms
// out. It is set on Zoom when Rect is empty.
Factor float64
// Delta is how far a Pan moved the view, in device units.
Delta ir.Point
// Rows are the source rows a Select covered, in the layer named by
// [Hit.Layer] of the panel named by Panel.
//
// A selection is reported one layer at a time, so a list of rows never has
// to say which layer's rows they are: a rectangle over two crossing series
// fires twice, and a handler that cares about one of them ignores the
// other by its layer rather than by unpicking a mixed list.
//
// The rows are the positions the *layer reported*, not the ink it drew,
// so a half-covered bar is a question about where its value is rather than
// about where its corner is. They need row tracking — see
// [Index.TrackRows] — and are empty without it.
//
// The slice is the event's own and is not reused between events, so a
// handler may keep it.
Rows []int
}
Event is one thing that happened to a chart.
It is one struct rather than one type per kind. CONCEPT.md §13 sketched separate HoverEvent and ZoomEvent types, and in Go that shape forces the handler through an `any` and a type assertion — so the kinds share a struct and each says which of its fields are meaningful. A handler registered for one kind never sees another, so the fields that do not apply are never read.
type EventKind ¶
type EventKind uint8
EventKind is what happened.
const ( // Hover is the pointer moving over the chart. It fires on every move, // whether or not a mark is under the pointer — [Event.Found] says which. Hover EventKind = iota // Leave is the pointer leaving the chart, or leaving every mark it was // over. A tooltip is dismissed here. Leave // Click is a press and release on the chart. Click // Zoom is a change of scale about a point, or into a rectangle. Zoom // Pan is a translation of the view. Pan // Select is a region of the chart the reader dragged out. It carries the // rectangle in [Event.Rect] and the rows under it in [Event.Rows], one // event per layer the region touched. // // It is last because the kinds before it are the ones a chart has always // had, and a constant that already means something must go on meaning it. Select )
The event kinds.
type Hit ¶
type Hit struct {
// Panel is which panel the mark is in, and Layer which layer of it.
Panel, Layer int
// Series is the layer's legend label, empty for a layer that has none.
Series string
// Kind is what sort of mark this is.
Kind Kind
// At is the mark's position in device space.
At ir.Point
// X and Y are the data values at At, read back through the panel's
// scales. On a categorical axis the value is the category index — pass it
// to [scale.Categorical.Labels] to name it.
X, Y float64
// Distance is how far At is from the point that was asked about, in
// device units. It is zero for a point inside an area.
Distance float32
// Hidden reports whether the series this hit's layer is currently turned
// off, and is only meaningful when Kind is [LegendRow]. It is what lets a
// handler say "show" or "hide" rather than having to ask the chart.
Hidden bool
// Value is the quantity a guide hit stands for: the value a colourbar's
// ramp reaches under the pointer, or the value a size key's sample is
// drawn for. It is meaningless for every other kind, which report their
// position through X and Y instead.
//
// A colourbar reads its value through the *ramp* rather than through the
// axis beside it. The two disagree wherever the ramp is compressed — a log
// ramp, a diverging one centred off zero — and the ramp is the thing the
// reader is pointing at.
Value float64
// Area is the rectangle of the guide that was hit — a legend row, a
// colourbar band, a whole continuous bar, a size key row. It is the empty
// rectangle for a hit on a mark, which has no target to speak of.
//
// It is what a caller anchors to: a tooltip beside a legend row, or the
// band a drag along a colourbar is painted in.
Area ir.Rect
// Class is which band of a classed colourbar was hit, or -1 for a hit on a
// continuous ramp and for every kind that is not a colourbar.
//
// Lo and Hi are that band's interval. They are what a filter is written
// against: a reader clicking the third band of a quantile scale means the
// rows between those two numbers, not the single value under their finger.
Class int
Lo, Hi float64
// Row is the source row behind the mark, or -1 when it is not known.
//
// It is -1 unless row tracking was on for the render — see
// [Index.TrackRows] — and it stays -1 for a mark that no row is behind: a
// boxplot's box aggregates many rows, a density raster is not a mark, an
// interpolated point across a gap was never measured, and a third-party
// geom that does not report its rows has none to report.
Row int
}
Hit is what the pointer found.
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is a spatial index over one render.
It implements the render package's Observer, and Index.Watch wraps the backend a chart is drawn into. Neither changes what is drawn.
An Index is not safe for concurrent use; nor is it safe to query while the render that fills it is still running.
func (*Index) At ¶
At reports the mark nearest a device point, within tol device units. A tol of zero uses DefaultTolerance.
Marks are ranked by how specific they are and then by distance: a point the pointer is near beats a shape it is merely inside, and a shape beats a label. Pointing at a scatter marker that happens to sit inside a bar and under an annotation reports the marker, because the marker is the row the reader is asking about. Within one rank, later marks win ties — a later mark was drawn on top and is the one that can be seen.
The search is linear in the marks on screen. That is the right shape here: a decimated chart draws thousands of marks, not millions, and a tree that has to be rebuilt every frame costs more to maintain than the scan costs to run.
func (*Index) ColorbarEntry ¶
func (ix *Index) ColorbarEntry(e render.ColorbarInfo)
ColorbarEntry implements the render package's optional ColorbarEntry: it records a colourbar, or one band of a classed one.
The colour scale is kept rather than the mapping, because where a value sits on a bar is the *ramp's* answer and not the axis's — the two disagree wherever the ramp is compressed. A hit inverts through it at the moment it is asked, which is the same thing a hit in a panel does through the panel's scales.
func (*Index) End ¶
func (ix *Index) End()
End implements the render package's Observer: it closes the layer that was open, so that the guides drawn after the data — and the chart's overlay after them — are not indexed as marks of whichever layer happened to be drawn last.
Without it the last Layer call stays the most recent thing this was told, and a legend swatch is indexed as a shape belonging to that layer. That was invisible for as long as the only way in was github.com/timzifer/figure.Live.Move, which does not hit-test a point outside every panel — but Index.At is reachable on its own, and an overlay draws *inside* a panel, where it would be hit.
func (*Index) LegendEntry ¶
func (ix *Index) LegendEntry(e render.LegendInfo)
LegendEntry implements the render package's optional LegendEntry: it records where a row of the legend was drawn, so that a pointer over it can be told which series it stands for.
The row is indexed as a mark of kind [Guide] in panel -1, because a legend belongs to the chart rather than to a panel and inverting its position through a panel's scales would report a value from a place no value was drawn. Hit.X and Hit.Y are therefore zero on a guide hit; Hit.Layer and Hit.Series are what it is for.
func (*Index) Locate ¶
Locate reports where a source row of a layer landed in the render just watched.
It is the inverse of Index.At, and it is the far end of the wire between two charts: the first says which row the pointer is on, the second says where that row is on screen — which is what a caller drawing a highlight ring, a crosshair or a leader line needs, without rebuilding anything.
ok is false when row tracking was off for the render (see Index.TrackRows), when the layer reported no row for this one, or when the row is not on screen — a decimated line draws the rows that survived, and a row that was reduced away is not somewhere the reader can be pointed at.
A row reported at more than one position — which no built-in mark does, but nothing forbids — reports the last, matching Index.At's rule that a later mark was drawn on top.
func (*Index) MarkCount ¶
MarkCount reports how many marks were indexed. It is what a test asserts on and what a caller watching memory looks at.
func (*Index) Marks ¶
Marks implements the geom package's Rows: it is how a layer reports where each of its source rows landed.
The slices are lent for the call — they come from the geom's pooled scratch — so the positions are copied out.
func (*Index) Reset ¶
func (ix *Index) Reset()
Reset empties the index, keeping its memory for the next render.
func (*Index) RowsIn ¶
RowsIn appends every row that landed inside r to dst and returns it, in paint order.
It is what a brush reads: the rectangle a reader dragged, and the rows under it. The test is against the position the *row* was reported at rather than the ink of the mark drawn through it, which is what makes a half-covered bar a matter of where its value is rather than of where its corner is — and is the same position Index.Locate hands back and Hit.Row resolves through.
dst is the caller's; pass dst[:0] to reuse it.
func (*Index) RowsOf ¶
RowsOf appends every row one layer reported to dst and returns it.
The order is the order the layer reported them in, which is the order it drew them. dst is the caller's, so a caller asking every frame keeps one slice and allocates nothing — pass dst[:0] to reuse it.
func (*Index) SizeKeyEntry ¶
func (ix *Index) SizeKeyEntry(e render.SizeKeyInfo)
SizeKeyEntry implements the render package's optional SizeKeyEntry: it records one row of a size key and the value its sample stands for.
func (*Index) TrackRows ¶
TrackRows turns row identity on or off and returns ix, so the call can be chained onto New.
It is off by default because it is not free: every layer that can report its rows does the bookkeeping, and the index keeps a position and a row number per mark on top of the marks it already keeps. Turn it on when a hit has to name a row rather than describe a point — highlighting the matching row of a table beside the chart is the case it exists for.
It takes effect on the next render, and only if the caller also hands the index to the renderer as its row sink; github.com/timzifer/figure.Live does that.
func (*Index) TrackingRows ¶
TrackingRows reports whether row identity is on.
type Kind ¶
type Kind uint8
Kind is what sort of mark a hit landed on.
const ( // Vertex is a point a layer drew: a line's sample, a scatter's marker. Vertex Kind = iota // Area is a filled shape: a bar, a band, a boxplot's box. Area // Label is text a layer drew. Label // LegendRow is a row of the legend: furniture a reader can act on rather than // data. A hit on one says which series a swatch stands for, not what is // under the pointer — see [Hit.Layer] and [Hit.Series]. // // It is spelled apart from [Label], which is text a *layer* drew and is a // mark like any other. LegendRow // Colorbar is a colourbar, or one band of a classed one. A hit on it says // which value the ramp reaches there — see [Hit.Value] — and, for a band, // which class and over what interval. Colorbar // SizeKey is a row of a size key. A hit on it says which value the sample // stands for. SizeKey )
The mark kinds. They differ in how a hit is decided: a vertex is hit by being near it, an area by being inside it.
type Panel ¶
type Panel struct {
// Area is the panel's plot rectangle in device space.
Area ir.Rect
// X and Y are the panel's scales, ranged to Area.
//
// Both are nil for a panel with no screen axes — a projected scene, whose
// marks were placed by a camera rather than by a pair of intervals. A
// caller inverting a device position checks first, as [Index.At] does, and
// reads the value out of [Hit.Row] instead.
X, Y scale.Scale
// Z is the panel's depth scale, non-nil only for a panel announced by
// [github.com/timzifer/figure/three]. ADR 0056 is the record.
Z scale.Scale
// Y2 and X2 are the panel's secondary vertical and horizontal axes, when a
// layer in it was drawn against one, and nil otherwise.
//
// They are discovered from the layers rather than announced with the
// panel: [render.Observer.Panel] carries the two scales a panel has always
// had and never gains more, so a second axis arrives through
// [Index.Layer] with the layer that reads it. A caller steering the
// chart needs them — a zoom that moved one axis and left the other in the
// same direction would slide the two series apart.
Y2 scale.Scale
X2 scale.Scale
// Coord is the coordinate system the panel was drawn in, framed to Area.
// It is what turns a device position back into the pair the scales speak
// in — without it a pointer over a pie slice would be inverted as though
// the wedge were a rectangle, and would report a value nothing was drawn
// at. It is [coord.Cartesian] for a chart that named no coord.
Coord coord.Coord
}
Panel is one panel of a watched render: where it is and what places values in it.
func (*Panel) Coords ¶
Coords is the panel's coordinate system, or coord.Cartesian for a panel recorded before there was one to record.
It is what a caller steering the chart needs as well as what a tooltip does: a wheel or a drag is a device position, and turning that into a change of domain means going back through the coord before the scales see it.
type RowRef ¶
type RowRef struct {
// Panel is which panel the row was drawn in, and Layer which layer of it.
Panel, Layer int
// Series is the layer's legend label, empty for a layer that has none.
Series string
// Row is the source row, in the table that was handed in.
Row int
// At is where the row landed, in device space.
At ir.Point
}
RowRef is one source row of one layer of one panel, and where it landed.
It is what the reverse of a hit test reports. Hit answers "what is under this point"; a RowRef answers "where did this row go", which is the question a second chart asks when the first one says which row the pointer is on.