Documentation
¶
Overview ¶
Package three draws a chart whose x, y and z are all data.
It is the second of the three features ADR 0055 split "3D" into: a surface over a grid, a trajectory through a volume, a field of bars over two categoricals — charts where the third dimension carries a reading the flat chart of the same table cannot. The camera that turns them is ADR 0057, and what the dimension is for is ADR 0058.
Shape of the API ¶
A Scene is what is plotted. A View is a camera on it. A Plot is the figure they are drawn into:
sc := three.NewScene(three.XTitle("x"), three.YTitle("y"), three.ZTitle("gain"))
sc.Z(scale.Linear(scale.Nice()))
sc.Add(three.Surface(src, geom.X("x"), geom.Y("y"), geom.Z("gain")))
p := three.New(three.Size(720, 520), three.Title("Response"))
p.Scene(sc)
err := p.Render(svg.File("surface.svg"))
The split is the point. A scene holds no camera, so several views share one scene and the scales behind it are trained once however many angles are drawn from them — one data repository, several ways of looking at it:
p.Add(
three.View{Camera: three.Home(), Label: "three-quarter"},
three.View{Camera: three.LookAt(three.Azimuth(0)), Label: "front"},
three.View{Camera: three.LookAt(three.Elevation(math.Pi / 2)), Label: "top"},
)
Why this is its own package ¶
github.com/timzifer/figure/render walks a panel's layers and each layer's Build streams straight into the backend, so a layer is a paint unit. A projected scene has no paint unit smaller than the view: a point can be in front of one part of a surface and behind another, so correct occlusion is a single depth order over the primitives of every layer at once. Producing that inside render would need either a depth on every drawing call or two drawing orders, and both are refused. So the draw loop, the ordering, the cube's furniture and the camera live here, beside render rather than through it.
A Layer is therefore not a github.com/timzifer/figure/geom.Geom, and that is a guard as much as a consequence: a geom handed a depth would ignore it and draw a flat line inside a projected box — correct by its own lights, wrong by the chart's and silent either way. Here it cannot happen, because geom.Line does not compile into a Scene. Layers do read geom's option set, though, so a channel is spelled the way it is spelled everywhere else.
The IR gains nothing ¶
There is no ir.Point3, no 4x4 in the IR and no depth on a drawing call. This package owns its own Vec3, its own matrix and its own Camera, projects everything above the seam, and calls Polyline, FillPath and Text with the plain two-dimensional coordinates those have always taken. So every backend draws a surface on the day this package compiles: SVG, PDF, canvas, raster, the native window, the GPU tier. See docs/adr/0056-three-dimensional-charts.md.
Painting over a finished figure ¶
Plot.Overlay and Live.Overlay install something to paint after every view is drawn: a ring round the rows a reader picked, a leader line, a caption. It is where a host draws a selection, because there is no selection in here — a scene with four cameras shares its data by holding one pointer and nothing propagates between the views, which is the division docs/adr/0045-linked-views.md made for two charts and docs/adr/0062-a-scene-and-its-views.md restated for two cameras.
An overlay is told where every view landed and, through OverlayView.At, the pixel any value of the data was drawn at in it — which is the only direction that exists here, since a device point in a turned cube resolves to no triple of values. It is drawn last, clipped by nothing, and announced to no observer, so nothing it paints is hit-testable. See docs/adr/0063-an-overlay-over-a-scene.md.
What it does not do ¶
No perspective: under one, the same value is taller at the front of the scene than at the back, and a chart is a measuring instrument first. No lighting model beyond one directional shade per face. No arbitrary meshes — a painter's order is exact only over a set that can be totally ordered, and two triangles that interpenetrate have no correct order at all. No windows, no handlers, no event loop: a camera is a value the caller holds, and turning it is the host's loop.
Index ¶
- Variables
- func Plane(p ContourPlane) geom.Option
- type Camera
- type CameraOption
- type ContourPlane
- type Describer
- type Frame
- type Highlight
- type Layer
- type Live
- func (l *Live) Camera(cam Camera) *Live
- func (l *Live) CameraOf(i int) Camera
- func (l *Live) CameraValue() Camera
- func (l *Live) Close() error
- func (l *Live) CurrentOverlay() Overlay
- func (l *Live) Draw() error
- func (l *Live) Home() *Live
- func (l *Live) Index() *interact.Index
- func (l *Live) Overlay(o Overlay) *Live
- func (l *Live) Rescale(dpr float64) error
- func (l *Live) Resize(w, h int) error
- func (l *Live) SetCamera(i int, cam Camera) *Live
- func (l *Live) Size() (w, h int)
- func (l *Live) TrackRows(on bool) *Live
- func (l *Live) ViewAt(x, y float64) int
- func (l *Live) ViewCount() int
- type Mark
- type Option
- type Overlay
- type OverlayFrame
- type OverlayView
- type Overlays
- type Plot
- func (p *Plot) Add(vs ...View) *Plot
- func (p *Plot) CurrentOverlay() Overlay
- func (p *Plot) CurrentScene() *Scene
- func (p *Plot) DataTable(w io.Writer) error
- func (p *Plot) Describe() a11y.Summary
- func (p *Plot) Description() ir.Description
- func (p *Plot) Live(t ir.Target) (*Live, error)
- func (p *Plot) Observer(o render.Observer) *Plot
- func (p *Plot) Overlay(o Overlay) *Plot
- func (p *Plot) Render(t ir.Target) (err error)
- func (p *Plot) Scene(s *Scene) *Plot
- func (p *Plot) Size() (w, h int)
- func (p *Plot) TrackRows(r geom.Rows) *Plot
- func (p *Plot) Views() []View
- type Point3
- type Projection
- type Scene
- type SceneOption
- type Sink
- type Style
- type Vec3
- type View
Constants ¶
This section is empty.
Variables ¶
var ErrCategorical = errors.New("figure/three: categorical column on a continuous scale")
ErrCategorical reports a text column mapped onto a continuous axis.
var ErrNoColumn = errors.New("figure/three: no column selected")
ErrNoColumn reports a layer that was not told which column to read.
var ErrNoScene = errors.New("three: the plot has no scene")
ErrNoScene is returned by a plot asked to draw before it was given something to draw.
Functions ¶
func Plane ¶ added in v0.10.0
func Plane(p ContourPlane) geom.Option
Plane chooses which face of the cube a Contour lies on.
The value travels as a string rather than as the constant, because a description round-trips through JSON and a number comes back as a float64 — a typed byte would not survive the trip, and the failure would be a chart that read back drawing on the wrong face.
Types ¶
type Camera ¶
type Camera struct {
// contains filtered or unexported fields
}
Camera is where a scene is looked at from.
It is an immutable value: Orbit, Dolly and Slerp each take one and return another, so the same inputs always give the same frame and a camera is testable without a surface. That is what lets the host own the drag — this package installs no handler, opens no window and runs no loop — and it is also what lets several [View]s hold several cameras onto one Scene.
It is orthographic, and stays so. Under perspective the same value is taller at the front of the scene than at the back, and a chart is a measuring instrument first. See docs/adr/0057-orbiting-a-chart.md.
The zero Camera is the front view at the default zoom. The angle a chart is designed at is Home, and it is the one a static export and a reader who cannot drag both get, so it has to be readable on its own.
A Camera is comparable, which is what lets a live chart ask cheaply whether the reader moved it.
func Dolly ¶
Dolly multiplies a camera's zoom and returns the result.
Under an orthographic camera the eye's distance from the scene changes nothing at all — not weakly, exactly nothing — so what a reader means by dollying here is a scale factor. That is why there is no Distance: a knob that does nothing is worse than no knob.
func Home ¶
func Home() Camera
Home is the three-quarter view a scene is drawn at when nobody chose.
ADR 0057 makes this a requirement rather than a default: a scene whose initial angle hides its own data behind itself is broken for everyone who cannot drag it, and for every static export — which is most of them. Live.Home is one call back to it.
func LookAt ¶
func LookAt(opts ...CameraOption) Camera
LookAt builds a camera from its angles.
cam := three.LookAt(three.Azimuth(-0.6), three.Elevation(0.35))
func Orbit ¶
Orbit turns a camera by two angle deltas and returns the result.
It is a pure function: same inputs, same camera, no state. The deltas are radians rather than pixels, because pixels per radian is a statement about how an interaction feels and that belongs to the host's input layer — as do inertia, momentum and springs, none of which are here. A host converts once, with its own constant:
const perPixel = 0.008 // radians; the host's choice, not figure's live.Camera(three.Orbit(live.CameraValue(), -dx*perPixel, dy*perPixel))
The signs are the ones a reader expects of taking hold of the scene: the side facing them follows the pointer. The azimuth grows anticlockwise seen from above, so a drag to the right, which carries the camera the other way, is a falling azimuth; device y grows downward, so a drag down lifts the camera.
Elevation is clamped just inside the poles; azimuth wraps.
func Slerp ¶
Slerp interpolates between two cameras and is how a host eases an orbit.
A camera has no rows, no keys and no domain, so interpolating between two of them is interpolating a view — which is exactly the tweening ADR 0044 refused for data, and is trivially correct for a view, because a view has no identity to lose. So easing a camera is a loop over this function and the easings in the root package, and it does not go anywhere near a transition: animating the camera and animating the data are different features that happen to both produce frames.
Azimuth takes the short way round. t at or below zero returns a exactly and t at or above one returns b exactly, which is what a golden test of an eased orbit's last frame depends on.
func (Camera) Forward ¶
Forward is the unit direction the camera looks along, in scene space.
It is what a layer that orders its own primitives reads: a surface's back-to-front traversal is the sign of these three components and nothing else. Depth increases along it, so a point further from the camera has the larger key.
type CameraOption ¶
type CameraOption func(*Camera)
CameraOption configures a Camera.
func Azimuth ¶
func Azimuth(rad float64) CameraOption
Azimuth turns the camera about the up axis, in radians.
func Elevation ¶
func Elevation(rad float64) CameraOption
Elevation raises the camera above the floor plane, in radians. It is clamped just inside the poles: a camera looking straight down its own up-vector has no up-vector, and the scene would spin on its axis at the moment the reader least expects it.
func Zoom ¶
func Zoom(f float64) CameraOption
Zoom sets how much of the view's cell the scene fills. One is the default fit and larger fills more.
type ContourPlane ¶ added in v0.10.0
type ContourPlane uint8
ContourPlane names the face a family of isolines lies on.
const ( // Floor is the bottom of the cube, which is where a plan belongs and is the // default. Floor ContourPlane = iota // Ceiling is the top, for a scene a reader is looking up into. Ceiling )
The faces a contour may lie on. See Contour on why the walls are not among them.
type Describer ¶
Describer is implemented by a Layer that can say what it plots.
It is optional for the reason github.com/timzifer/figure/geom.Describer is optional: a layer nobody describes does not have to know what a screen reader is. A layer that implements none is named by its position rather than skipped, because a description that quietly omits a series is worse than one that admits to it.
type Frame ¶
type Frame struct {
// X, Y and Z are the trained scales, ranged into the unit cube.
X, Y, Z scale.Scale
// Theme supplies the defaults the layer did not override, and the light a
// shaded face is lit by.
Theme theme.Theme
// Index is the layer's position in the scene, which is what picks its
// default colour out of the palette.
Index int
// View is which view is being emitted for, and Forward the unit direction
// its camera looks along. A layer that walks its own geometry in depth
// order reads Forward: a surface's traversal is the signs of its three
// components and nothing else.
View int
Forward Vec3
// Rows, when non-nil, collects which source row is behind each mark. It is
// nil for an ordinary render, and a layer reports rows by naming them on
// the sink rather than by calling anything here — see [Sink.Row].
Rows geom.Rows
}
Frame is everything a layer needs in order to turn its data into primitives. It is github.com/timzifer/figure/geom.Frame's counterpart, and the difference between them is the whole of Layer's doc comment: this one carries no backend, because a layer here does not draw.
type Highlight ¶ added in v0.10.0
type Highlight struct {
// Marks are the places to ring, in device space, each saying whether it is
// behind something. Each is drawn in the view whose Area contains it, or in
// View when one is named; a mark in no view is not drawn.
Marks []Mark
// Data are points in the data, rung in every view. That is the default
// rather than a setting: a figure with four cameras is one chart looked at
// four ways, so a value marked in one of them is marked in all four.
//
// They are always drawn solid. A value is a place in the cube rather than
// something the scene drew, so there is nothing for it to be behind — and a
// caller who does mean a drawn mark has [Mark] and its answer.
Data []Point3
// View confines the rings to one view, or -1 for the rule above.
//
// Minus one is the useful setting and not the zero value, which is the
// trade [github.com/timzifer/figure.Highlight] makes for the same reason:
// nought is a real view, so a zero that meant "any" would leave no way to
// say "the first".
View int
// Radius is the ring's radius in device units. Zero takes six, a little
// larger than a default marker.
Radius float32
// Color and Width override the theme. A zero Color takes the theme's label
// colour and a zero Width takes two device units — a ring wants to read as
// an annotation rather than as data.
Color ir.Color
Width float32
// Dash is the pattern a hidden ring is drawn with. A nil one takes a short
// even dash, which is what a hidden edge is drawn with.
Dash []float32
// contains filtered or unexported fields
}
Highlight rings a set of marks, to say "these ones".
It is the reason this seam exists. A hit on a projected scene reports which layer and which row and leaves its X and Y at zero — there is no pair of values to read back out of a turned cube — so a host that knows *which row* a reader picked has no way of its own to say where that row landed. It asks github.com/timzifer/figure/interact.Index.Locate once per view and rings every answer, and the same measurement is then marked in the three-quarter, the plan and both profiles at once.
A ring behind something is dashed ¶
A scene hides its own far side, so a mark may be behind the surface it belongs to. Such a ring is drawn dashed and the rest solid, which is the convention an engineering drawing has used for a hidden edge for as long as there have been engineering drawings — and this figure is already in that register, since ADR 0062's whole argument for several views is the plan and the two elevations a machinist reads.
Dashed rather than inverted, and the reason is the same one that makes the rings an overlay at all: an inverted ring would have to know what colour the thing in front of it was drawn in, which means reading pixels back — and what it read would be the shade of one face rather than a statement about depth. A dash says the one thing that is true, which is that something is in front.
Whether a mark is hidden is the host's to decide, because only the host has the hit index the answer comes out of. See Mark.
The zero value draws nothing. A Highlight holds the paths it drew last, so one belongs to one figure — install a second for a second.
func (*Highlight) DrawOverlay ¶ added in v0.10.0
func (h *Highlight) DrawOverlay(b ir.Backend, f OverlayFrame)
DrawOverlay implements Overlay.
type Layer ¶
type Layer interface {
// Train feeds the layer's data into the three scales so they can
// establish their domains. It runs before layout, because layout needs
// tick labels and tick labels need a domain.
//
// It runs once per frame however many views are drawn, because a domain is
// a fact about the data and not about where the data is looked at from.
// The struct is geom's because that is the seam ADR 0056 widened for
// exactly this: it gained a Z, and every two-dimensional geom kept
// compiling.
Train(t geom.Training) error
// Emit appends the layer's primitives, in scene space, to s.
//
// It runs once per view, because which order a layer emits in is a fact
// about the camera: a surface walks its lattice from the corner the view
// direction picks.
Emit(s *Sink, f Frame) error
// Legend returns the entry this layer contributes, or ok == false if it
// should not appear in one.
Legend(f Frame) (geom.LegendEntry, bool)
}
Layer is one set of marks in a scene.
It is deliberately not a github.com/timzifer/figure/geom.Geom. A geom's Build streams ink straight into the backend, which makes a layer a paint unit — and a projected scene has no paint unit smaller than the view, since a point can be in front of one part of a surface and behind another. So a layer here emits primitives and this package projects, orders and paints what every layer emitted, once, over all of them at a time.
The separation is a guard as well as a consequence. A geom handed a depth would ignore it and draw a flat line inside a projected box — correct by its own lights, wrong by the chart's, and silent either way. Here that cannot happen: geom.Line is not a Layer and does not compile into a Scene.
Stability ¶
Layer is implemented outside this module, so it never gains a method, and each method takes one parameter beyond its destination. What a layer can additionally do arrives as an optional interface beside it. ADR 0060.
func Bar3 ¶
Bar3 draws one box per cell of two categorical axes.
Read the heatmap first ¶
Bars occlude each other, the back row is unreadable, and the height of a bar behind another cannot be compared to it. The flat chart of the same table — github.com/timzifer/figure/geom.Rect with a colour scale, which is what a heatmap is — wins almost every time. This ships because refusing it invites a worse reimplementation by every caller, and its doc comment says so because a reader who chose wrong is a reader who was not told. See docs/adr/0058-what-3d-is-for.md.
three.Bar3(src, geom.X("service"), geom.Y("region"), geom.Z("latency"))
Both floor axes want a github.com/timzifer/figure/scale.Ordinal: this is a chart of two categoricals, and a continuous axis under it draws boxes at arbitrary widths.
The three faces a box shows are one primitive each and they carry the same source row, so a pointer anywhere on a bar reports that row. They are ordered by the cell the bar stands on rather than by the middle of the box, so a tall bar never draws itself in front of the short one standing between it and the reader.
func Contour ¶ added in v0.10.0
Contour draws the level sets of a value on the floor of the cube.
It is the reading a surface hides, put where a reader can take it: the shape stands above and its plan lies beneath, in one picture and at one angle. That is what a topographic map does with a mountain, and what a machinist's plan view does with a part.
sc.Add(
three.Surface(src, geom.X("x"), geom.Y("y"), geom.Z("z"), geom.ColorBy("z", ramp)),
three.Contour(src, geom.X("x"), geom.Y("y"), geom.Z("z"),
geom.Levels(lv...), geom.ColorBy("z", ramp)),
)
Handed the same github.com/timzifer/figure/geom.Levels and the same colour scale, it is the same lines a flat github.com/timzifer/figure/geom.Contour draws — one tracing in stat.Contour rather than two, which is what stops a reading taken off the plan and one taken off the floor disagreeing at a saddle.
The floor, or the ceiling, and not the walls ¶
Plane chooses which face the lines lie on, and the two faces it offers are the two the value is a function *over*. A back wall is a plane that contains z, so there is no isoline to draw on it — projecting the floor family sideways would draw lines that mean nothing. What a wall wants is a cross-section of the surface at that wall's coordinate, which is a different layer with a different stat.
What it costs ¶
One drawing call per segment, which is Sink.Line's rule: a run that spans the floor has no single depth under any camera that is not looking straight down, so a whole run drawn as one primitive would be ordered wholly in front of the surface or wholly behind it. Per segment it interleaves properly. That is a real cost on a fine grid — keep the level count small, which is what its default is for.
func Line3 ¶
Line3 draws an ordered path through x, y and z.
It is the trajectory: an orbit, a tool path, an IMU track, an attractor — a curve that crosses itself in every flat projection of it and not in the data. See docs/adr/0058-what-3d-is-for.md.
three.Line3(src, geom.X("t"), geom.Y("offset"), geom.Z("power"))
github.com/timzifer/figure/geom.GroupBy draws one path per series, and that is all a cascade is: a family of traces offset along one floor axis, which is the display a spectrum analyser has had since the seventies. It is a recipe rather than a mark of its own, which is why there is no Cascade here — see examples/cascade.
A path is emitted one segment at a time rather than whole. A whole path is one primitive with one depth, and a curve that spans the scene has no single depth, so a trajectory crossing a surface would be drawn wholly in front of it or wholly behind. Per segment it interleaves at every scale a reader can see. What it still cannot do is pass *through* a surface within one segment: splitting a primitive along an intersection is a BSP tree, which is a renderer, and this package draws the shapes whose order is decidable.
func Surface ¶
Surface draws z = f(x, y) over a regular grid.
It is the chart the third dimension is for. A heatmap of the same grid gives the values and hides which way the ground falls; a surface gives the shape of the response between its samples — a ridge, a saddle, the edge of a plateau. See docs/adr/0058-what-3d-is-for.md.
three.Surface(src, geom.X("x"), geom.Y("y"), geom.Z("gain"))
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 an error out of Train rather than a picture with holes in it: a surface drawn over a scattered sample is a surface over a triangulation nobody asked for.
Shading is one directional light from the theme and nothing else — the face's normal against github.com/timzifer/figure/theme.Theme.LightDir, mixed in linear light. github.com/timzifer/figure/geom.ColorBy paints from the height through a ramp instead, which is the terrain reading, and the two compose: the ramp gives the height and the shade gives the slope. There is no colourbar beside it and none is missing — the depth axis is the key, and it is drawn with ticks and a title.
Decimation is off, and github.com/timzifer/figure/geom.Decimate is accepted and ignored. A reduction defined over pixel columns measures nothing in a projected scene, where one column of screen mixes values from everywhere along the view direction; a grid the caller chose the size of is the caller's to choose smaller.
type Live ¶
type Live struct {
// contains filtered or unexported fields
}
Live is a scene drawn into a surface that can be redrawn and turned.
It mirrors figure.Live's shape and keeps its split: a method that changes the view and does not draw returns nothing, and a method that draws returns an error. So turning a scene is two calls —
live.Camera(three.Orbit(live.CameraValue(), -dx*perPixel, dy*perPixel)) live.Draw()
— and that is what leaves the host owning its own loop. This package installs no handler, opens no window and does not know where the two floats came from.
It wants a surface rather than a document. The SVG and PDF emitters build a document and write it whole, so drawing many frames into one collects every frame in the same file; use Plot.Render for those. There is no interactive SVG and there will not be one: emitting a document with a script that re-projects the scene in the viewer means shipping a second renderer, in another language, inside a file.
A Live is not safe for concurrent use.
func (*Live) Camera ¶
Camera points every view at cam.
This is ADR 0057's spelling, and on a figure with several views it turns all of them together — a synchronised orbit of a front / top / side figure is one statement about the chart. Live.SetCamera turns one.
func (*Live) CameraOf ¶
CameraOf reports one view's camera, or the zero camera for an index that is not a view.
func (*Live) CameraValue ¶
CameraValue is the first view's camera, which on a figure with one view is its camera.
func (*Live) CurrentOverlay ¶ added in v0.10.0
CurrentOverlay reports what is painting over the figure, or nil.
func (*Live) Draw ¶
Draw records a frame and paints what changed.
What a redraw costs ¶
A frame whose camera moved is not comparable with the last one: every drawing call in the turned view differs, because every point moved. Diffing two whole recordings to arrive at an answer known before it started is work nobody asked for, so a turn damages the views that turned and takes the diffing path when the cameras are at rest — where it earns its keep exactly as it does in a flat chart, because a changing dataset under a fixed camera is the case damage was designed for. See ADR 0016 and ADR 0057.
Expressing it that way rather than as a "a drag is in flight" flag is deliberate. A flag is state the library cannot verify, it is wrong for a programmatic eased orbit that no pointer is involved in, and a host that forgets to turn it off pays for a full repaint forever with nothing failing. A camera moved or it did not, and this package knows which.
func (*Live) Home ¶
Home returns every view to the camera its author chose.
A reader who has turned a scene into a mess is one call from the picture the chart was designed at, which is the other half of ADR 0057's requirement that the default camera be readable on its own.
func (*Live) Overlay ¶ added in v0.10.0
Overlay installs something to paint over the figure, and returns l so the call can be chained onto Plot.Live. Passing nil removes it.
It takes effect on the next Live.Draw. An overlay is a pointer to a struct whose fields the caller then moves — a ring's positions, a selection — so installing it once and mutating it per event is the intended shape and there is nothing to re-install. But this package installs no handler and runs no loop, so mutating one does not redraw by itself: call Live.Draw, as you do after Live.Camera.
Cost ¶
A turn with an overlay installed repaints the whole canvas rather than the cells whose cameras moved, because an overlay draws where it likes and no cell list describes what it damaged. So install the feedback a gesture needs for as long as the gesture lasts and take it away afterwards: one kept installed through an orbit costs a full repaint on every frame of the drag. A figure with no overlay pays exactly what it paid before.
func (*Live) Rescale ¶
Rescale tells the figure its surface's device pixel ratio has changed, and redraws it.
func (*Live) Resize ¶
Resize tells the figure its surface has changed size, and redraws it. The cameras keep whatever they were turned to.
func (*Live) TrackRows ¶
TrackRows turns row identity on or off and returns l, so the call can be chained onto Plot.Live.
It is off by default because it is not free: with it on, every layer records where each of its rows landed and the hit index keeps a position and a row number per mark. Without it interact.Hit.Row is -1 — and in a projected scene that is the whole answer a pointer has, because there are no screen axes to invert a device point through.
type Mark ¶ added in v0.10.0
type Mark struct {
// At is where to draw, in device space — typically what
// [github.com/timzifer/figure/interact.Index.Locate] answered.
At ir.Point
// Hidden says something in the scene is in front of this point, so the ring
// is drawn dashed rather than solid.
Hidden bool
}
Mark is one place to draw a ring, and whether the reader can see what is there.
Hidden is the half that is easy to leave out and wrong to. A projected scene occludes itself: the point a reader picked out may be on the far side of the surface it is on, and a ring drawn plainly over it says "this is here" when what is here is a piece of surface with the marked point somewhere behind it. The reader then reads a position off the near face that is not the position they picked.
type Option ¶
type Option func(*Plot)
Option configures a Plot.
func Columns ¶
Columns sets how many views sit side by side before the next row. It is one by default, which is what a figure with a single camera wants.
func Description ¶
Description sets what the chart says about itself in words, for a backend that can carry it.
It is camera-independent, and that is a requirement rather than a convenience: a reader using the description gets the same chart as a reader dragging the scene, and every static export says what the turned one says. See docs/adr/0057-orbiting-a-chart.md.
type Overlay ¶ added in v0.10.0
type Overlay interface {
// DrawOverlay paints over the finished figure.
DrawOverlay(b ir.Backend, f OverlayFrame)
}
Overlay paints over a finished figure: a ring round the row another view is pointing at, a leader line out to a caption, a mark a host placed itself.
Why it is not a layer ¶
The reason is github.com/timzifer/figure/render.Overlay's — a layer's positions come from its data through the scales, and an overlay's come from somewhere a layer has no access to: a pointer, a selection, or where the previous frame put something. This package adds a second reason of its own. An overlay is not in the depth order, and that is deliberate rather than a shortfall: its positions have no depth to be sorted by, and a mark inside the order would be occluded by the very surface it exists to point at.
It is announced to no github.com/timzifer/figure/render.Observer and is therefore not hit-testable, which is the property that makes it usable — a ring a pointer can hit is a ring that flickers, because hovering it moves the pointer off whatever the ring was about.
Why it is not render's type ¶
The method and its shape are render's, spelled again here rather than shared, which is the trade drawStrip already makes: the two are the same idea and not the same drawing. A flat overlay is told each panel's X and Y scales and the coord that framed them, because that is how a crosshair finds a threshold on screen. A view is told three scales, a Camera and a Projection — and a projected scene announces its panels with X and Y nil on purpose, because there are no screen axes to invert a device point through. Sharing the type would mean a struct half of whose fields are nil in one of the two places it is used, a caller having to know which half, and github.com/timzifer/figure.Crosshair compiling against a scene it can say nothing about.
What it costs ¶
A figure with no overlay pays nothing. One that only moves is a damage rectangle like anything else, because Live.Draw compares the frame it just recorded with the last whenever no camera moved. But a *turn* with one installed repaints the whole canvas rather than the cells that turned — an overlay draws where it likes, so no cell list describes what it damaged — so install feedback for the gesture it belongs to and take it away afterwards.
An Overlay is implemented outside this package, so it never gains a method.
type OverlayFrame ¶ added in v0.10.0
type OverlayFrame struct {
// Canvas is the whole drawing, in device space.
Canvas ir.Rect
// Views are the figure's views, in the order [Plot.Add] gave them.
Views []OverlayView
// Theme is the figure's theme, so that an overlay over a dark scene is
// legible on it.
Theme theme.Theme
}
OverlayFrame is what an overlay is told about the figure it draws over.
func (OverlayFrame) View ¶ added in v0.10.0
func (f OverlayFrame) View(i int) (OverlayView, bool)
View returns the view with the given index, for an overlay that was told one by a hit rather than by a position.
func (OverlayFrame) ViewAt ¶ added in v0.10.0
func (f OverlayFrame) ViewAt(pt ir.Point) (OverlayView, bool)
ViewAt reports which view contains a device point, for an overlay deciding which cell a pointer is in. It is Live.ViewAt's answer, from inside a draw.
type OverlayView ¶ added in v0.10.0
type OverlayView struct {
// Index is the view's position in the figure, which is the index
// [Live.ViewAt] reports, the one a
// [github.com/timzifer/figure/render.Observer] was told, and the one
// [github.com/timzifer/figure/interact.Hit] carries as its Panel.
Index int
// Label is the view's strip label, or "" for a view that has none — so an
// overlay can say which camera it is annotating without holding the [View].
Label string
// Area is the view's cell: the rectangle the scene was clipped to and the
// one [Live.ViewAt] matches against. An overlay is not clipped, so an
// overlay that wants a clip pushes this.
Area ir.Rect
// Inner is the rectangle the cube was actually fitted into — Area less the
// room the tick labels and axis titles need round it. It is what an overlay
// placing a caption beside the cube measures against.
Inner ir.Rect
// Camera is where this view looks from. [Camera.Forward] is how an overlay
// decides whether what it is ringing faces the reader.
Camera Camera
// X, Y and Z are the scene's three scales, which are ranged into the unit
// cube rather than into Area: what turns a scene point into a device point
// is the camera. They are the scene's own objects rather than copies, read
// in the frame that drew them, and must not be modified.
X, Y, Z scale.Scale
// Project is the projection this view was drawn with.
Project Projection
}
OverlayView is one view of the frame an overlay draws over: where it is, what it looks at the scene from, and what turns a value into a place in it.
func (OverlayView) At ¶ added in v0.10.0
func (v OverlayView) At(x, y, z float64) (ir.Point, bool)
At maps a point in the data through the view's three scales and projects it, which is what an overlay marking a value calls.
ok is false when any of the three scales has no position for its value — a log axis has none for zero — so nothing is drawn at a coordinate that means nothing. A value outside a domain is clamped into the cube, which is what this package's own layers do with one.
type Overlays ¶ added in v0.10.0
type Overlays []Overlay
Overlays draws several overlays in order, so that a figure can have more than one without either knowing about the other. Later ones are on top, and a nil member is skipped — so a caller may keep a fixed-length list and switch one off by clearing it.
func (Overlays) DrawOverlay ¶ added in v0.10.0
func (os Overlays) DrawOverlay(b ir.Backend, f OverlayFrame)
DrawOverlay implements Overlay.
type Plot ¶
type Plot struct {
// contains filtered or unexported fields
}
Plot is a figure: one or more [View]s of a Scene, at a size, in a theme.
It is not a figure.Plot and the root package re-exports nothing of this one. figure's whole job is resolving a plot into a render.Chart, and a scene is not one — no coordinate system, no two-dimensional panels, no geoms — so a re-export would put names in the root package whose methods did not work on the type that lives there. That is the second path through one seam that ADR 0056 spends a version to avoid, moved to the root package.
What is shared is the vocabulary: a scale is a scale, a theme is a theme, a channel is spelled with geom's options, and a target is an ir.Target — so a scene renders into every backend figure has without the root package being in the import graph at all.
func (*Plot) CurrentOverlay ¶ added in v0.10.0
CurrentOverlay reports what is painting over the figure, or nil.
func (*Plot) CurrentScene ¶ added in v0.10.0
CurrentScene reports the scene the plot draws, or nil for a plot whose views each brought their own.
It is spelled apart from Plot.Scene because that name is taken by the setter, which is the one a caller writes; this is for a host that has to read a layer back — to ask what a row it was handed is called, say — and would otherwise have to be given the scene a second time. View.Scene is the per-view override to resolve it against.
func (*Plot) DataTable ¶
DataTable writes the scene's rows as HTML tables — the third of the three channels a chart says what it is in, and the one a projected scene leans on hardest: a reader who cannot turn the picture still gets every number in it.
func (*Plot) Describe ¶
Describe reads the plot's scene and says what it shows.
**It does not mention the camera, and that is a requirement rather than an omission.** A reader using the description gets the same chart as a reader dragging the scene; the description of a figure with four views is the description of what is plotted, once. A chart that must be turned to be read is a chart some readers cannot read, so the words and the data table have to be complete without it. See docs/adr/0057-orbiting-a-chart.md and docs/adr/0024-accessibility.md.
func (*Plot) Description ¶
func (p *Plot) Description() ir.Description
Description is what a backend that can carry a description is told, before anything is drawn.
func (*Plot) Live ¶
Live opens t and returns the scene drawn into it. The target stays open until Live.Close.
func (*Plot) Observer ¶
Observer is told which view and which layer is drawing, so that a caller wrapping the backend can attribute a mark to the layer that made it. It is how hit-testing is built without widening the IR, and it is the same interface render announces through — so interact.Index needs no change to index a projected scene.
func (*Plot) Overlay ¶ added in v0.10.0
Overlay installs something to paint over the figure, and returns p so the call can be chained. Passing nil removes whatever was there.
It is a method rather than an Option because an overlay is an attachment like Plot.Observer and Plot.TrackRows rather than a statement about how large the figure is or how it looks, which is the split Plot already draws.
A Live opened from this plot inherits it, because Plot.Live copies the plot. The copy is also why the inheritance goes one way only: installing one on the Live afterwards does not reach the plot the caller still holds, and installing one on the plot afterwards does not reach the Live.
func (*Plot) Render ¶
Render draws the plot once into t.
It wants a document or a surface indifferently: a single frame is the same call either way. A scene redrawn frame after frame into one surface is Plot.Live.
type Point3 ¶ added in v0.10.0
type Point3 struct{ X, Y, Z float64 }
Point3 is a position in the data, which is what an overlay names when it wants a mark in every view.
It is three float64 and not a Vec3 because none of it is a scene coordinate: it is a value on each of three axes, and a view's scales are what turn it into geometry.
type Projection ¶ added in v0.10.0
type Projection struct {
// contains filtered or unexported fields
}
Projection turns a point of the scene into a point on the surface.
It is a value copied into every OverlayView rather than a function field or an interface, and the reason is the allocation gate: a closure is an allocation per view per frame, and boxing one into an interface is another. A value is neither, and it is comparable, so a caller can ask cheaply whether a view's projection moved.
There is no exported matrix, for the reason ADR 0057 gives: a matrix is a second way to build a camera — one that can be sheared, non-orthonormal or perspective, all of which this package refuses.
func (Projection) Depth ¶ added in v0.10.0
func (pj Projection) Depth(v Vec3) float64
Depth reports how far a scene point is from the camera along the view direction. Larger is farther, which is the order a painter walks.
func (Projection) Point ¶ added in v0.10.0
func (pj Projection) Point(v Vec3) ir.Point
Point projects a scene-space point: a Vec3 in the unit cube, which is what a Layer emits into a Sink. Reach for OverlayView.At to start from a value instead.
func (Projection) Valid ¶ added in v0.10.0
func (pj Projection) Valid() bool
Valid reports whether this projection came from a drawn view. The zero Projection maps every point to one place, so an overlay handed a frame it built itself checks this rather than drawing a pile of marks on top of each other.
type Scene ¶
type Scene struct {
// contains filtered or unexported fields
}
Scene is what is plotted: the layers, and the three scales they train.
It is the data half of a three-dimensional chart, and it holds no camera. That split is the whole arrangement: a View is a camera on a scene, several views share one scene, and the scales behind it are trained once however many angles are drawn from them. One data repository, several ways of looking at it.
It holds no theme and no chart title either. A theme is how the *figure* looks and two views in one figure must share it; a chart title names the figure. Both belong to Plot. The three axis titles belong here, because an axis names the data and every view of one scene labels the same three.
A *Scene is shareable and is not safe for concurrent modification, which is the contract figure.Plot has.
func (*Scene) SetLayers ¶
SetLayers replaces the layers, which is what a scene that gains a highlight layer on every pointer move needs so that it does not gain one per move.
type Sink ¶
type Sink struct {
// contains filtered or unexported fields
}
Sink collects the primitives of one view of a scene, in scene space.
Vertices go into one flat arena and a primitive is a range in it, which is what makes a frame cost a handful of allocations over a surface of any size: the arena, the primitive list and the sort's key slice all come from a pool and are refilled rather than rebuilt. ADR 0057 pre-commits this package to that discipline and the allocation gate holds it to it.
A Sink is handed to a layer's Layer.Emit and is not retained afterwards.
func (*Sink) Face ¶
Face appends a filled face: three or more corners, in order around it.
The face is drawn as one closed subpath, which is what makes it one mark to a hit index — a quad of a surface is a thing the reader can point at.
func (*Sink) Forward ¶
Forward is the unit direction the camera looks along, which is the same vector Frame.Forward carries. It is here as well so that a helper handed only a sink can pick its traversal.
func (*Sink) Line ¶
Line appends a stroked run through vs.
A layer that draws a path emits one Line per segment rather than one per path. A whole path is one primitive with one depth, and a curve that spans the scene has no single depth — so a trajectory that crossed a surface would be drawn wholly in front of it or wholly behind.
func (*Sink) Row ¶
Row attributes the primitives that follow to a source row, or to -1 for one that no row is behind — an interpolated point, a wall, a summary. It holds until it is changed and is reset to -1 for each layer.
It costs nothing when nobody is listening: a scene rendered without row tracking never reads what a layer reported.
func (*Sink) Text ¶
Text appends a run anchored at a scene point.
Only the anchor is projected. A label lying in a projected plane needs a shear and ir.TextRun has none — deliberately, since a backend shapes its own runs — so the text stays upright, which is also easier to read than the sheared alternative every serious 3D tool ends up offering to turn off.
type Style ¶
type Style struct {
// Fill is the interior colour, and is transparent for a line.
Fill ir.Color
// Stroke is the outline colour, and is transparent for a face drawn
// without one.
Stroke ir.Color
// Width is the outline's width.
Width float32
}
Style is the paint of one primitive.
It carries no dash. A dash is a length on screen, and a projected run's length on screen is not its length in the data, so a dashed line in a scene says less than it appears to; when one is asked for, this gains a field. Being comparable is what lets adjacent primitives batch into one drawing call with ==.
type Vec3 ¶
type Vec3 struct{ X, Y, Z float32 }
Vec3 is a point or a direction in scene space.
Scene space is the unit cube: x and y span the floor, z is up, and every coordinate lies in [0, 1] because that is the interval the three scales are ranged into. A scale maps a value into an interval without caring what the interval means — the property ADR 0018 was built on — and here the interval is an edge of a box rather than an edge of a panel.
The components are float32 because github.com/timzifer/figure/ir.Point is, so the projection converts nothing on the hot path.
func (Vec3) Cross ¶
Cross returns the vector perpendicular to both v and w, which is how a face gets the normal its shade is computed from.
func (Vec3) Dot ¶
Dot returns the scalar product, in float64.
The width is deliberate and it is not symmetry with the rest of the package: this is the arithmetic behind a depth key, a depth key feeds a comparison, and a comparison is a decision. AGENTS.md records what one float32 ulp did to a decision once already.
type View ¶
type View struct {
// Camera is where this view looks from. The zero value is the front view;
// [Home] is the angle a chart is designed at, and the one a static export
// and a reader who cannot drag both get.
Camera Camera
// Label is written in a band above the view, or "" for none. It is what
// names a cell of a front / top / side figure.
Label string
// Scene overrides the plot's scene for this view, and is nil for the views
// that share it — which is the ordinary case and the one this whole
// arrangement is for.
Scene *Scene
}
View is one camera on a scene.
It is a struct with exported fields because it grows: a per-view layer mask, a clip to a sub-box of the cube, a per-view theme override are all fields when they arrive rather than a second View type. ADR 0060 is the rule.
Where a view lands in the figure is Plot's to say rather than the view's, because a row and a column on a struct have no honest zero value — nought, nought is a real cell. Views are placed in the order they were added, across the rows of a grid whose width is Columns.