interact

package
v0.8.0 Latest Latest
Warning

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

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

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

View Source
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

	// 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
}

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.

func (Event) Series

func (e Event) Series() string

Series is the label of the layer under the pointer, empty when there is no hit. It is the field a tooltip reaches for first, so it is spelled out rather than left as Hit.Series behind a Found check.

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
)

The event kinds.

func (EventKind) String

func (k EventKind) String() string

String names the kind, for tests and error messages.

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

	// 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 New

func New() *Index

New returns an empty index.

func (*Index) At

func (ix *Index) At(pt ir.Point, tol float32) (Hit, bool)

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) Layer

func (ix *Index) Layer(i int, label string)

Layer implements the render package's Observer.

func (*Index) MarkCount

func (ix *Index) MarkCount() int

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

func (ix *Index) Marks(at []ir.Point, rows []int)

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) Panel

func (ix *Index) Panel(i int, area ir.Rect, x, y scale.Scale, cd coord.Coord)

Panel implements the render package's Observer.

func (*Index) PanelAt

func (ix *Index) PanelAt(pt ir.Point) (int, bool)

PanelAt reports which panel contains a device point.

func (*Index) Panels

func (ix *Index) Panels() []Panel

Panels reports the panels of the last watched render, in chart order.

func (*Index) Reset

func (ix *Index) Reset()

Reset empties the index, keeping its memory for the next render.

func (*Index) RowCount

func (ix *Index) RowCount() int

RowCount reports how many marks carry a source row.

func (*Index) TrackRows

func (ix *Index) TrackRows(on bool) *Index

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/refract.Live does that.

func (*Index) TrackingRows

func (ix *Index) TrackingRows() bool

TrackingRows reports whether row identity is on.

func (*Index) Watch

func (ix *Index) Watch(b ir.Backend) ir.Backend

Watch returns a Backend that draws into b and indexes what it draws.

Only marks emitted inside a layer are indexed: the grid, the axes, the titles and the guides are furniture, and a pointer landing on a grid line has not landed on anything a reader would ask about.

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
)

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.
	X, Y 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 added in v0.8.0

func (p *Panel) Coords() coord.Coord

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.

Jump to

Keyboard shortcuts

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