Documentation
¶
Overview ¶
Package coord maps scaled positions into device space.
A scale maps a data value into an interval; a coord decides what that interval means. Cartesian says it is a distance along an edge of the panel and is the identity, so every geom draws what it always drew. Polar says one of the two intervals is an angle and the other a radius, and the same geoms then draw a pie, a donut, a radar, a rose or a gauge.
That is the whole of it: the coord is a stage between the scales and the IR, and neither end changes. github.com/timzifer/refract/scale.Scale is untouched — what used to be Cartesian was only that render passed the panel rectangle's edges as the interval — and the IR is untouched too, because an arc is cubics and github.com/timzifer/refract/ir.Path has always had those. See docs/adr/0018-coordinate-systems.md.
What a coord does not do ¶
It does not paint. Coord.Furniture reports where a panel's grid lines, axis lines and tick labels go and render strokes them, because render is the only package that knows the drawing order of a chart. A coord that drew its own rings would be a second drawing order.
It does not own a panel either. A coord belongs to a chart, and Coord.Frame hands back the coord positioned in one panel's rectangle rather than moving the receiver into it — panels are built concurrently, and a coord that remembered which panel it was in would be a data race.
Index ¶
Constants ¶
const FullTurn = 2 * math.Pi
FullTurn is the default sweep: a whole circle.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Coord ¶
type Coord interface {
// Frame gives the coord a panel rectangle and that panel's scales, sets
// the interval each scale maps into, and returns the coord positioned in
// the rectangle. Cartesian sets the rectangle's edges; Polar sets an angle
// range and a radius range.
//
// The receiver is not modified: the returned value is what the panel's
// geoms are handed, so two panels drawn on two goroutines never share one
// position.
Frame(area ir.Rect, x, y scale.Scale) Coord
// Extent reports the interval each scale maps into — what Frame chose.
// A mark that spans a whole axis needs it: the far end of a rule is where
// the scale ends, and under a polar coord that is not where the rectangle
// does.
Extent() (x0, x1, y0, y1 float32)
// Point turns one mapped pair into a device point.
Point(x, y float32) ir.Point
// Points is the batch form, and the one a geom on the hot path calls. It
// appends into dst, which the caller owns and reuses between frames.
//
// It exists so that a per-row interface call does not reappear here: a
// variadic or per-row method on this interface is the shape that cost a
// million allocations on a million-row column once already.
Points(dst []ir.Point, xs, ys []float32) []ir.Point
// Straight reports whether an edge that is straight in data space is also
// straight on screen. It is true for Cartesian and for a polar coord asked
// for chords; it is false for one drawing arcs, where a geom has to build
// a path instead of a polyline.
Straight() bool
// Edge appends the device path of an edge that is straight in data space,
// continuing from p's current point. Cartesian appends one LineTo; Polar
// appends the cubics of an arc.
Edge(p *ir.Path, from, to ir.Point)
// Area appends the closed device path of a data-space rectangle, given as
// two mapped pairs. Cartesian appends four corners; Polar appends an
// annular sector.
Area(p *ir.Path, x0, y0, x1, y1 float32)
// Clip appends the path a panel's data is clipped to: the rectangle, or
// the disc inscribed in it.
Clip(p *ir.Path, area ir.Rect)
// Invert turns a device point back into a mapped pair, which is what a
// tooltip needs before it asks the scales what the values were.
Invert(pt ir.Point) (x, y float32)
// Furniture reports the geometry of the grid lines, the axis lines and the
// tick labels of one panel, one entry per tick in tick order. It fills dst
// rather than returning a value so that a chart redrawn every frame does
// not pay for its furniture again; render resets and reuses one.
Furniture(dst *Furniture, area ir.Rect, m Metrics, xTicks, yTicks []scale.Tick)
// Decimates reports whether a reduction defined over pixel columns still
// measures what it was defined to measure under this coord.
//
// It is true for Cartesian, where [github.com/timzifer/refract/stat.LTTB]
// and MinMax bucket by a column of screen and that is exactly the unit
// they were designed in. It is false for Polar, where a bucket of equal
// angle is not a bucket of equal width — and nothing polar is a big-data
// chart, so saying no costs nothing. See docs/adr/0011-decimation.md.
Decimates() bool
}
Coord turns a pair of mapped positions into a device point, and reports the geometry of everything else that depends on what the pair means.
Implementing one is how a coordinate system is added. The set of methods is wider than a transform because a coordinate system is wider than a transform: what a straight edge is, what a data-space rectangle is, what a panel clips to, and where a grid line runs are all answers only the coord has.
func Cartesian ¶
func Cartesian() Coord
Cartesian is the identity coord, and the default.
Its Point returns the pair it was given, its Edge is one LineTo and its Area is four corners — so a chart that never heard of this package draws exactly what it drew before there was one, and the golden files in the repository are the proof.
func Donut ¶
Donut is Pie with a hole of the given fraction: the same chart with its middle left empty.
refract.Coord(coord.Donut(0.45))
The hole is where the radial scale starts rather than a disc of background painted over the middle, so nothing is drawn inside it and a pointer in it hits nothing — see Hole. A donut whose slices name their own inner and outer radius is that hole plus github.com/timzifer/refract/geom.X2: the radial axis is a dimension like any other, and the ring is only the slot a slice fills when the row does not say.
func Pie ¶
Pie is Polar with the angle taken from the Y axis: the recipe above, named.
It is sugar and nothing else — `Pie()` is `Polar(Theta(FromY))` — but it is the spelling that says what the chart is, and the one place the two things a pie needs beyond the coord can be written down where a reader will look for them. Those two are the layer and the scales:
p := refract.New(refract.Coord(coord.Pie()))
p.X(scale.Linear()) // one slot, filling the radius
p.Y(scale.Linear()) // the stacked total, filling the circle
p.Add(geom.Bar(src, geom.X("one"), geom.Y("share"), geom.GroupBy("browser")))
Neither scale is niced, and that is not an oversight: a niced angular domain rounds the total up and leaves a wedge of nothing at twelve o'clock.
Further options are applied after the default, so a half pie is `Pie(Sweep(math.Pi))` and a pie that turns the other way is `Pie(Counterclockwise(true))`.
func Polar ¶
Polar wraps one axis around a circle and reads the other as a radius.
It is what turns the marks that already exist into the family of charts that did not: a pie and a donut are a stacked github.com/timzifer/refract/geom.Bar with θ from the Y axis, a rose is the same bar with θ from an ordinal X, a radar is a github.com/timzifer/refract/geom.Line over one, and a gauge is a bar over a partial sweep. None of them is a new geom, which is the whole point of the stage.
Which axis is the angle ¶
By default X sweeps the circle and Y is the radius, which is what a rose, a wind rose and a radar want: the category or the direction goes round and the magnitude goes out. Theta swaps them, which is what a pie wants: the value goes round and the single slot goes out.
A pie ¶
p := refract.New(refract.Coord(coord.Polar(coord.Theta(coord.FromY))))
p.X(scale.Linear()) // one slot, filling the radius
p.Y(scale.Linear()) // the stacked total, filling the circle
p.Add(geom.Bar(src, geom.X("one"), geom.Y("share"), geom.GroupBy("browser")))
The ring closes into a full circle because a stacked Y domain ends at the total and starts at zero — so the angular scale must not be niced. A niced domain rounds the total up and leaves a wedge of nothing at twelve o'clock, which is why the recipe above spells scale.Linear without scale.Nice. Hole turns the pie into a donut.
Edges ¶
An edge between two marks is an arc by default, because that is the edge that is straight in data space and it is what a rose petal and a polar band need. A radar is the exception — its sides are chords, and drawing them as arcs bows them outwards — so a radar asks for Chord.
type Desc ¶
type Desc struct {
// Type is which coord this is.
Type Type
// Theta is the axis a polar coord sweeps around the circle.
Theta Axis
// Hole is the inner radius as a fraction of the outer one, zero for a
// coord with no hole.
Hole float64
// Radius is how much of the panel's shorter half-side the circle fills.
Radius float64
// Start is where the angular scale begins, in radians clockwise from
// twelve o'clock, and Sweep how much of the circle it covers.
Start, Sweep float64
// Counterclockwise reverses the direction the angular scale runs in.
Counterclockwise bool
// Chord reports a coord drawing an edge between two marks as the straight
// line between them rather than as an arc.
Chord bool
}
Desc is a coord reduced to what configures it.
It is the bargain github.com/timzifer/refract/scale.Desc makes, for the same reason: a Coord is an interface over an unexported type, which is right for mapping positions and useless for writing one down. Nothing about a coord is a Go function, so unlike a scale, nothing here is lost.
func Describe ¶
Describe reports c's configuration, or ok == false if c cannot describe itself. A third-party coord that does not implement Describer still draws; it is simply not serializable.
func (Desc) Default ¶
Default reports whether d describes the coord a chart has when nobody chose one. A document does not carry a field for that: the absent coord and the Cartesian one draw the same chart, and writing `"coord": {"type": "cartesian"}` into every spec refract has ever produced would be noise.
type Describer ¶
type Describer interface {
Describe() Desc
}
Describer is implemented by a coord that can write itself down, so that a chart survives the round trip through the JSON spec. It sits beside github.com/timzifer/refract/scale.Describer and is optional for the same reason: a coord nobody serialises does not have to know what JSON is.
type Exploder ¶
type Exploder interface {
// Explode reports the device displacement of a mark whose extent in the
// space the scales map into is the given pair of mapped positions, when it
// is broken out by the fraction by of the coord's outer radius.
Explode(x0, y0, x1, y1 float32, by float64) (dx, dy float32)
}
Exploder is implemented by a coord with a middle for a mark to be moved away from: what a slice broken out of a donut is doing.
It is an optional interface, like Describer, and Cartesian deliberately does not implement it. A rectangle on a Cartesian panel has no direction to be broken out in — every bar would move the same way, which is a translation of the layer rather than a reading of it — so a layer asking to break its marks out under a coord that has no middle draws exactly what it drew, and nothing is silently invented.
The displacement is answered rather than applied, because a coord does not draw: the geom that built the mark moves the path it built. A geom resolves the interface once per Build rather than per mark, exactly as it does github.com/timzifer/refract/scale.Band. See docs/adr/0026-breaking-a-mark-out.md.
type Furniture ¶
type Furniture struct {
// AxisX and AxisY are the two axis lines.
AxisX, AxisY Shape
// GridX is one shape per X tick and GridY one per Y tick, in tick order.
// A tick with no grid line — a minor one, or one outside the panel — has
// an empty shape.
GridX, GridY []Shape
// TickX and TickY are the tick marks, in tick order, empty where there is
// none.
TickX, TickY []Shape
// LabelX and LabelY are where the tick labels go, in tick order.
LabelX, LabelY []Label
// InX and InY report, per tick, whether the tick falls inside the panel at
// all. A Cartesian coord culls a tick that a float32 mapping put a hair
// outside the plot rectangle; a polar one has nothing to fall off.
InX, InY []bool
// horizontal line and can therefore run into each other. render drops the
// ones that would overlap when they do — and must not when they do not:
// two labels on opposite sides of a ring can share an x and still be a
// finger apart.
XLabelsShareARow bool
}
Furniture is the geometry of one panel's grid lines, axis lines, tick marks and tick labels. A coord fills it; render strokes it.
Every per-tick slice is parallel to the tick list it came from, so index i is tick i whether or not that tick is drawn. That is what lets render keep the decisions that are its own — which grid lines the theme asked for, which labels would collide, whether this panel writes labels at all — while the coord answers only where things go.
It is filled rather than returned so that a chart redrawn every frame does not allocate its furniture again: Furniture.Reset keeps every buffer.
type Label ¶
type Label struct {
At ir.Point
H ir.HAlign
V ir.VAlign
// Rotation turns the label about its anchor, in radians clockwise. It is
// zero for every label a Cartesian axis writes.
Rotation float64
}
Label is where one tick label sits and how it is aligned about that point.
type Metrics ¶
type Metrics struct {
// TickLen is how far a major tick mark reaches out of the axis, and
// MinorTickLen the same for a minor one.
TickLen, MinorTickLen float32
// LabelPad is the gap between the end of a full-length tick mark and the
// label beyond it.
LabelPad float32
}
Metrics are the theme lengths a coord needs in order to place furniture. They are passed in rather than read because a coord must not know what a theme is.
type Option ¶
type Option func(*polar)
Option configures a coord.
func Arc ¶
func Arc() Option
Arc is the default edge policy, spelled out for a chart that wants to say so. See Chord for the other one.
func Chord ¶
func Chord() Option
Chord draws an edge between two marks as the straight line between them rather than as the arc through data space. It is what a radar wants: the sides of a spider chart are chords, and an arc between two axes bows the outline outwards into something the data does not say.
func Counterclockwise ¶
Counterclockwise runs the angular scale the other way round. The default is clockwise, which is the direction a pie, a clock and a compass all read in.
func Hole ¶
Hole leaves the middle of the circle empty, as a fraction of the outer radius in [0, 1). It is what makes a donut out of a pie and a ring gauge out of a gauge, and it is an explicit annulus rather than a white circle painted over the middle: the hole is where the radial scale starts, so a mark never enters it and a pointer in it hits nothing.
func Radius ¶
Radius sets how much of the panel the circle fills, as a fraction of half its shorter side. The default leaves room outside the ring for the tick labels that go round it; a chart with none — a pie usually has none — can ask for the whole of it.
func Start ¶
Start turns the whole coord about its centre, in radians clockwise from twelve o'clock. The default is zero: the first slice of a pie and the first axis of a radar both begin straight up, which is where a reader looks first.
type Shape ¶
type Shape struct {
// Pts is a straight run, empty when the shape is curved.
Pts []ir.Point
// Path is a curve, empty when the shape is straight. It is a value rather
// than a pointer so that its buffers survive [Furniture.Reset].
Path ir.Path
}
Shape is one piece of furniture: a straight run of points, or a path when the coord bends it.
The two forms are not interchangeable and the difference is deliberate. A Cartesian grid line is two points and reaches the backend as a Polyline, exactly as it did before there was a coord to ask — which is why every golden file in the repository still matches. A polar ring is cubics and reaches it as a stroked path.