canvas.graph

canvas.graphNode

A plotting environment on the scene's canvas. Its body holds marks — scatter, curve, line, the shapes, fit and the surfaces — and the parameters that drive them, each of which gets a slider in the environment footer. It draws its own frame, and the x and y domains are animatable with !cue.to.

#The graph environment

canvas.graph is a plotting surface: its body holds marks, it draws its own frame, and its domains are animatable. It carries the environment's id — canvas.graph#projectile{...} — and the frame is set by x-domain / y-domain (derived from the data when omitted), x-axis-label / y-axis-label, show-grid, show-axes, show-ticks, and show-background as the master switch. show-ticks: false hides the axis's own numbers and nothing else — axis.label names stay, and the gutter shrinks to what is left in it.

#Planes

A graph's head names its plane — what a coordinate on its marks is. canvas.graph:xy is the ordinary plane and the default; canvas.graph:x a number line, with one axis and marks that take x alone (a scatter there is a dot plot, repeated values stacking, and a boxplot takes a lane above the line); canvas.graph:argand the complex plane, whose axes are re and im and whose marks take one complex z point(z: 3 + 2i), point(z: (3 + 2i)^2), with i a constant and every expression complex-valued — or re and im apart, which is how two columns bind; canvas.graph:polar, whose axes are r and theta, the angle in radians unless written 60deg. A pair — a segment's ends, an angle's corner — is (a, b) on every plane, read in the plane's own axes.

canvas.graph:argand{
    vector(z: 3 + 2i; label: $z$)
    point(z: (3 + 2i)^2; label: $z^2$)
    circle(z: 0; size: abs(3 + 2i); fill: false)
    curve(z: sqrt(13) * e^(i * t); t: [0, 2pi])
    axis.label(im: 2; label: $2i$)
}(
    re domain: [-4, 14]
    im domain: [-6, 14]
)
canvas.graph:polar{
    curve(expression: 1 + cos(theta))
    point(r: 2; theta: 60deg)
    axis.band(r: [1, 2])
    axis.band(theta: [0, pi/4])
}(
    r domain: [0, 3]
)

A plane admits the marks it can draw and nothing else — the compiler says which when one is refused, and why. The argand plane has no independent axis, so fit, integral and curve: f are not drawn there (a curve on it is z(t)); the polar plane refuses integral, whose area is not the one it computes; the number line draws positions and series and nothing that needs a second axis. A bar on the polar plane is a rose, standing at names on theta; a histogram there is a wind rose, its bins edges on theta. The same rules hold for a mark inside a cue. Every plane's axis attributes are spelled with the axis's name in front — re domain:, r domain:, theta format: — and the other planes' are refused.

#Axes

An axis has two properties, written with its name in front and combining with each other and with any plane. x format: is what the axis holds: numbers; radians, which sets each label as a fraction of π and rules the axis in π — its automatic grid step comes off the π ladder, π/4, π/2, π, 2π; degrees, whose values are still radians and whose labels read 30°; dates, which places 2024, 2024-03, 2024-03-15 and a date column at their fractional years and ticks by span; the preset series months and weekdays (ticks Jan…, Mon…); and a written list — x format: Red, Blue, Yellow — or a dataset column — x format: #sales.region, its distinct cells in row order. A series makes the axis categorical: the names stand at positions 1…n, every mark on the axis speaks them (line(x: Mon, Tue, Wed; y: 12, 19, 15)), a column of names binds directly, and a name the axis does not have is a compile error. A continuous mark — curve, fit, integral — is refused on a categorical axis: there is nothing between two names to draw it over.

auto, the default, is decided from the marks: a categorical series on the axis rules it in those names, in the order they first appear; a date column or literal rules it in dates; otherwise the axis holds numbers. Write the values out when the order or the full set matters — a Likert scale, a category with no data that must still appear.

x scale: is how the axis places a value: linear, log (v at log₁₀ v) or log2. A transform, not a relabelling — the data is written as measured, scatter(x: 10, 100, 1000), and the axis puts each reading at its logarithm: curves are sampled in it, a fit on log–log gives the exponent as its slope, axis.band(x: [100, 1000]) is one decade, domains and !cue.zoom windows stay in data units, and the ticks are the decades written as values. A literal at or below zero is a compile error; a cell at or below zero drops its row. A scale beside names, dates or angles is refused, and it is never auto.

canvas.graph{
    scatter#readings(x: 10, 100, 1000; y: 3, 9, 27)
    fit(of: #readings; degree: 1)
}(
    x scale: log
    y scale: log
)
canvas.graph{
    @data#sales{
        region | y2023 | y2024
        North  | 38.0  | 42.1
        South  | 33.5  | 31.4
        East   | 47.2  | 55.0
        West   | 26.1  | 27.8
    }

    bar(x: #sales.region; y: #sales.y2023; colour: grey; label: 2023)
    bar(x: #sales.region; y: #sales.y2024; colour: blue; label: 2024)
}(
    y axis label: revenue (£k)
)
canvas.graph#fit-graph{
    scatter#observations(x: 1, 2, 3, 4; y: 2.0, 2.4, 3.1, 4.8; colour: grey)
    fit(of: #observations; model: polynomial; degree: 1; colour: teal)
}(
    x-axis-label: x
    y-axis-label: y
)

#Marks

Everything drawn on the surface is a mark, and marks share a set of attributes: colour, label (rich text, so $maths$ sets properly), hidden (declared but not drawn — and animatable), lock (protects the mark on an editable: graph), and layer (paint order). The shapes are line, segment, vector, point, polygon, circle, square, rectangle, triangle, star, diamond, hexagon, plus the angle between two of them and freestanding text. Every outline takes dashed, which is how a figure draws the shape that is not there — where it was before it moved, a region a constraint rules out, a construction that explains the drawing without being part of it.

Three of them carry a figure's notation rather than its data. A segment takes ticks, the congruence hatches across its middle: two sides marked ticks: 1 are the same length, which is how a construction says so — the notation angle already has in arcs. brace spans two points with a curly brace and hangs its name at the tip — brace(start: (0, 0); end: (4, 0); label: $R$) — which is what axis.brace is for an interval on an axis, for a distance anywhere in the picture. It bows to the left of the direction it is written in, so swapping its ends (or flip) moves it to the other side, and its depth is in pixels, like an angle's radius. And scatter takes error and error-x, one half-width per reading — a column of uncertainties bound whole draws a capped bar through every dot, and the frame grows to hold the caps.

#Curves

A curve plots an expression in x: curve(expression: x^2; colour: blue) — in theta on the polar plane, where it is r(θ). Expressions may reference parameters — a * sin(b * x) bends live as the reader drags a and b — and domain / range clip where it draws. A curve exposes derived max and min over its plotted domain. Or a curve is a position in t: curve(x: cos(t); y: sin(t); t: [0, 2pi]) on the ordinary plane, z(t) on the argand one, r and theta in t on the polar one — with the interval t runs over, and no expression.

#Data marks

scatter takes its readings one axis per attribute, paired by position — scatter(x: 1, 2, 3; y: 2.0, 2.4, 3.1) — and either attribute binds a dataset column directly: scatter(x: #islands.area; y: #islands.species). line and polygon read x / y the same way, and both take interpolate: linear joins the readings, bezier rounds the corners off with a cubic through every one of them, and a line alone takes step, which holds each value until the next x and then jumps. A rounded line claims the quantity varied smoothly in between, so it belongs on a trajectory and not on a tally. bar reads x / y the same way and stands a bar at each reading: one axis must be categorical (the axis of names is the orientation), the other reaches the baseline. Several bars at one name split the band in source order; stack stands a bar on the one before it instead; width is the share of the band the bars fill. histogram is the other rectangle mark and a different thing: its bins are edges, so its width is data, and it draws on a numeric axis. It takes the readings themselves — histogram(x: #sample.height; bins: 12) — and bins them, bins saying how many; parametised (bins: k) the reader re-bins the sample for themselves, which is the quickest way to see how much of the shape was the choice of grid. The coordinate the readings are written on is the axis the bins land on and so which way the bars grow (x up from the x axis, y out from the y axis, theta a rose on the polar plane). A sample already tallied is written the other way instead — values the counts and bins the edges they fall between, one more number than there are counts, with orientation for the direction no coordinate is there to say.

lollipop is the third of them and is for a quantity that stands at a point rather than filling an interval: a stem from the baseline with a dot at its end. lollipop(x: 0, 1, 2; y: 0.25, 0.5, 0.25) is a mass function written out, and it is the honest picture of one — P(X = k) belongs to the single value k, and a bar drawn there claims a width the quantity has not got. It takes the two forms a histogram does: a position and a value paired by position, or one axis alone, which is the readings — lollipop(x: #rolls.face) stands a stem of height three at every value that occurs three times, a tally by distinct value that invents no grid at all, which is what discrete data deserves instead of bins. A categorical axis is where the stems stand (names have no height), and with two numeric axes orientation says which way they run. The @data page covers where columns come from and which consumers bind them directly.

On the number line the same readings are a distribution. scatter(x: 2, 3, 3, 4) stacks equal values into a dot plot, and jitter spreads them across the line instead — a share of the mark's lane, each dot held at its own offset — which is what a sample of a few hundred readings has to be drawn as. On the xy and argand planes the same attribute nudges both coordinates, since both are the reading's own: the nudge is a share of the smallest non-zero gap between the values on each axis, at most half a gap either way, so tied readings come apart without any dot crossing the place a different reading would have been. It is refused on the polar plane, where an amount of theta is a different distance at every radius. boxplot takes the readings themselves and works the summary out: the box spans the quartiles, the median is ruled across it, and the whiskers run to the readings inside Tukey's fences, with whatever lies beyond drawn as its own dot (whiskers: extremes runs them to the smallest and largest instead, and !outliers keeps the fences but leaves those dots off — animatable, so a step can fade them in over a box the reader has already read). The quartiles are the hinges a class is taught to find — the median splits the ordered readings in two, itself in neither half when the count is odd, and each quartile is the median of its half. The marks that need room across the line take a lane each in source order, the first sitting on the line — except that a box and a scatter of the same readings are one sample drawn twice and share one, so writing both puts the summary over the readings with nothing to position by hand. Which is drawn over which is source order and layer, as for any pair of marks.

canvas.graph:x{
    scatter(x: #trial.mass; jitter: 0.7; colour: grey)
    boxplot#control(x: #trial.mass; colour: teal; label: control)
}(
    x axis label: mass (g)
)
A boxplot exposes derived median, q1, q3, iqr, min and max, so prose citing {{#control.iqr}} reads the width of the box beside it. min and max are the sample's extremes whether or not a whisker reaches them.

On a plane a box says which coordinate its readings are — x: and it lies along the x axis, y: and it stands up the y axis — and on: where it stands on the other one. A number there stands it at that value; a name stands it at that name and rules the axis in the names its boxes stand at, so three samples side by side need no x format: line. The room a box gets is the band at its value — a unit on an axis of names, and on a continuous one the closest two boxes stand, so no box can overlap another — and width: is its share of that room. Several boxes standing at one value split it between them in source order, as several bar marks split the band at one name.

canvas.graph:xy{
    boxplot(y: #trial.control; on: control; colour: blue)
    boxplot(y: #trial.treated; on: treated; colour: orange)
}(
    y axis label: mass (g)
)

Written with both coordinates it is one bivariate sample, paired reading by reading as a scatter's are, and what it draws is the rectangle of the hinges — the x quartiles by the y quartiles — with the two medians crossed inside it, a whisker from each edge out to that axis's fence, and every reading outside either fence as its own dot. It is what a scatter looks like once it is summarised, and it composes with one: written over the same columns, the box lands on the very dots it summarises. A box of a pair of samples publishes its numbers under each axis's own name — {{#cloud.x-median}}, {{#cloud.y-q3}} — and no plain median, which a sample of pairs does not have. On the argand plane the pair is re: and im:, or one complex z:.

canvas.graph:xy{
    scatter#cloud(x: #trees.height; y: #trees.mass; colour: grey)
    boxplot(x: #trees.height; y: #trees.mass; colour: teal)
}

#Fits and residuals

fit lays a least-squares fit through one scatter's observations, named in of: — its head slot, so fit: #observations is the same as fit(of: #observations). Give degree a parameter instead of a number and the reader drives it — Interaction shows that pairing. residuals draws the vertical differences from a fit to the points it was found from: residuals(of: #trend; colour: orange), or just residuals where the graph holds a single fit.

A fit fits the mark it names and nothing else, so a graph can carry two of them: draw the sample twice, once whole and once without the point in question, and the two lines stand beside each other with their own slope and r2. To fit part of a sample, draw that part as its own scatter and fit that.

canvas.graph{
    scatter#all(x: 1, 2, 3, 4, 5; y: 2.1, 3.9, 6.2, 7.8, 14.0; colour: grey)
    scatter#typical(x: 1, 2, 3, 4; y: 2.1, 3.9, 6.2, 7.8; colour: blue)

    fit#with-outlier(of: #all; degree: 1; colour: grey)
    fit#without(of: #typical; degree: 1; colour: blue)
}
A fit exposes derived slope, intercept and r2. The slope is the fitted polynomial's derivative at zero — exact, and live under whatever degree is in force — so prose citing {{#f.slope}} cannot disagree with the graph.

#Integrals

integral shades the area under a referenced curve between two bounds — integral(of: #bell; from: -1; to: 1) — and exposes the computed area as derived value. Its bounds may be expressions, so a parameter can sweep the region while the reader watches the value change.

#Surfaces

Two marks plot a function of the plane — the one place y is legal in an expression alongside x. contour draws level sets, either levels: 8 evenly-spaced or at: 1, 2, 4 exactly; heatmap paints the field itself. Both take a colour that is a single hue or a ramp — multicolour (the full spectrum) or temperature (blue through white to red) — and expose derived min and max over the visible window.

canvas.graph{
    heatmap(expression: sin(x) * cos(y); colour: temperature)
    contour(expression: sin(x) * cos(y); at: 0; colour: grey)
}

#Angles

angle marks the angle at a vertex and measures it: angle(at: (1, 1); from: (4, 1); to: (1, 4)) is the corner of a triangle, drawn with the arc — or, at a right angle, with the square a reader was taught to look for. An arm is written as either of the two things an arm is: a point it runs through, (4, 1), or a bearing in degrees anticlockwise from the positive x axis, 30. sweep: chooses which of the four ways round is meant — minor (the corner, and the default), reflex, or the signed anticlockwise / clockwise pair a sector or a rotation needs.

canvas.graph{
    polygon(x: 0, 4, 0; y: 0, 0, 3; colour: grey)
    angle#a(at: (0, 0); from: (4, 0); to: (0, 3))
    angle#b(at: (4, 0); from: (0, 0); to: (0, 3); show-value: true; label: $\theta$)
}
An angle publishes derived value (degrees) and radians, and show-value: true draws the reading on the plot. Both are the angle as drawn, so a caption citing {{#b.value}} cannot disagree with the arc beside it — including while a keyframed arm is still sweeping round.

#Axis annotations

axis.label names a value on an axis — axis.label(x: 3.1416; label: $\pi$) puts π where the number would have gone, and line: true adds a rule across the plot at that value. A name on the y axis is measured into the gutter beside it, so a worded one gets the room it needs; past about a quarter of the frame it wraps onto a second line rather than growing wider. axis.brace names an interval: a curly brace spanning x: [m, m + s] with its label at the tip, drawn in the gutter beside the tick numbers. axis.band shades an interval across the whole plot — a reference band the marks are read against, axis.band(label: recession; x: [2008, 2009.5]) — drawn under the grid and the marks. Which axis any of them speaks to is chosen by which coordinate or interval is given.

Regions are shaded two ways. integral takes the area under a function between two limits, and base says what it closes down to: the axis, unless it names another curve (base: #demand) or an expression of its own — base: 6 shades down to that height. The area between two curves is not a mark of its own because it is not an idea of its own, and the reported value follows the floor. A parametric curve takes fill instead, since its path can enclose something: a closed loop fills itself, and an arc closes across its own chord — close names a point to come back through first, so an arc closed through the centre of its circle is the sector. A y(x) curve is refused both: the region under a function is an integral, and saying so keeps one idea in one place.

#Derived attributes

Marks that compute something publish it for reading: a curve's max and min, a fit's slope, intercept and r2, an integral's value, a surface's min and max, a boxplot's median, q1, q3, iqr, min and max (under each axis's name where it summarises a pair of samples), a parameter's value. Reach one by member access on the mark's id — #f.slope in an attribute, {{#f.slope}} in prose. Derived attributes covers how they behave.


#Attributes

On any plane

planeImplicit
What a coordinate on the marks IS: x a number line, xy the ordinary plane (the default), argand the complex plane (re/im, or one complex z), polar (r/theta). The head form — canvas.graph:polar{}. A plane admits the marks it can draw and withdraws the other planes' axis attributes; a mark inside a cue is held to it just the same.Can be written as shorthand: canvas.graph:value
Defaultxy
The environment's accessible name — read by a screen reader, never drawn (the axis labels name a graph on the page). Rich text, so $maths$ and inline formatting render. A named attribute: the head : names the plane.
editable
The reader may add their own elements, and pan or zoom the frame.
Defaultfalse
interactive
Let the reader explore: scroll to zoom, drag to pan, double-click to reset — and zoom / reset controls appear in the toolbar.
Defaultfalse
show-ticksAnimatable
Also written: ticksShow tick labels.
Defaulttrue
show-axesAnimatable
Also written: axesShow the axes.
Defaulttrue
show-backgroundAnimatable
Also written: background, show-bg, bgMaster switch for the frame (grid, axes, ticks).
Defaulttrue
show-gridAnimatable
Also written: gridDraw the grid — the lines or dots the marks are measured against; on the polar plane, the rings at the r ladder and the rays at the θ one.
Defaulttrue
grid-styleAnimatable
lineslines-minordotsdots-minor
Also written: grid-typeWhat the grid is made of: 'lines' is squared paper, 'dots' steps back and lets the marks carry the picture. The -minor variants add unlabelled subdivisions between the labelled lines.
Defaultlines
continue
Inherit an earlier same-type environment's state: continue: #that-environment.
Only withxplane
x-domainAnimatable
Also written: domain, x-rangeX range, e.g. [0, 10]. Derived from the data when omitted. Animatable: x-domain: [0, 5] !cue.to{[0, 10]}(at: 1; over: 800ms). For a magnification, !cue.zoom draws a box around the new window and moves the frame into it: x-domain: [0, 10] !cue.zoom{[2, 4]}(at: 2; draw: 700ms; hold: 250ms). Endpoints may use parameters — y-domain: [0, 0.46 / s] frames the environment live. On a categorical axis the ends are names: [Tue, Thu]. The ordinary plane and the number line; the other planes name their own axes (re-domain, r-domain…).
x-formatAnimatable
Also written: x-labelsWhat the x axis holds. numbers; radians (ruled in π — the automatic grid step comes off the π ladder, ticks set as fractions of π); degrees (values in radians, ticks written 30°); dates (ISO literals 2024, 2024-03, 2024-03-15 and date columns at their fractional years, ticks by span); the preset series months and weekdays (ticks Jan…, Mon…; 1 = January, 1 = Monday); a written list (Red, Blue, Yellow) or a dataset column (#sales.region, its distinct cells in row order). A series makes the axis categorical: the names stand at positions 1…n, every mark on the axis speaks them, a column of names binds directly, a name the axis does not have is a compile error, and a continuous mark — curve, fit, integral, contour, heatmap, residuals — is refused. auto, the default, is decided from the marks: a categorical series rules the axis in its names, a date column or literal makes it dates, otherwise numbers.
Defaultauto
x-scale
How the x axis places a value: linear, log (v at log₁₀ v) or log2. A transform, not a relabelling: curves are sampled in it, a fit on log–log gives the exponent as its slope, axis.band(x: [100, 1000]) is one decade, domains and !cue.zoom windows stay in data units, ticks are the decades written as values (10, 100, 10⁴ past ten thousand). A literal ≤ 0 is a compile error; a cell ≤ 0 drops its row. Refused beside names, dates or angles.
Defaultlinear
x-grid-spacingAnimatable
Also written: x-grid, x-spacingGrid step on x, or auto.
Defaultauto
x-axis-label
Also written: x-labelX axis name.
Only withxyplane
x-domainAnimatable
Also written: domain, x-rangeX range, e.g. [0, 10]. Derived from the data when omitted. Animatable: x-domain: [0, 5] !cue.to{[0, 10]}(at: 1; over: 800ms). For a magnification, !cue.zoom draws a box around the new window and moves the frame into it: x-domain: [0, 10] !cue.zoom{[2, 4]}(at: 2; draw: 700ms; hold: 250ms). Endpoints may use parameters — y-domain: [0, 0.46 / s] frames the environment live. On a categorical axis the ends are names: [Tue, Thu]. The ordinary plane and the number line; the other planes name their own axes (re-domain, r-domain…).
y-domainAnimatable
Also written: range, y-rangeY range. Derived from the data when omitted. Animatable, with !cue.to or !cue.zoom — write the same anchor on both domains and the two axes move as one window. Endpoints may use parameters — y-domain: [0, 0.46 / s] frames the environment live. On a categorical axis the ends are names: [Tue, Thu]. The ordinary plane and the number line; the other planes name their own axes (re-domain, r-domain…).
x-formatAnimatable
Also written: x-labelsWhat the x axis holds. numbers; radians (ruled in π — the automatic grid step comes off the π ladder, ticks set as fractions of π); degrees (values in radians, ticks written 30°); dates (ISO literals 2024, 2024-03, 2024-03-15 and date columns at their fractional years, ticks by span); the preset series months and weekdays (ticks Jan…, Mon…; 1 = January, 1 = Monday); a written list (Red, Blue, Yellow) or a dataset column (#sales.region, its distinct cells in row order). A series makes the axis categorical: the names stand at positions 1…n, every mark on the axis speaks them, a column of names binds directly, a name the axis does not have is a compile error, and a continuous mark — curve, fit, integral, contour, heatmap, residuals — is refused. auto, the default, is decided from the marks: a categorical series rules the axis in its names, a date column or literal makes it dates, otherwise numbers.
Defaultauto
y-formatAnimatable
Also written: y-labelsWhat the y axis holds — as x-format.
Defaultauto
x-scale
How the x axis places a value: linear, log (v at log₁₀ v) or log2. A transform, not a relabelling: curves are sampled in it, a fit on log–log gives the exponent as its slope, axis.band(x: [100, 1000]) is one decade, domains and !cue.zoom windows stay in data units, ticks are the decades written as values (10, 100, 10⁴ past ten thousand). A literal ≤ 0 is a compile error; a cell ≤ 0 drops its row. Refused beside names, dates or angles.
Defaultlinear
y-scale
How the y axis places a value — as x-scale.
Defaultlinear
x-grid-spacingAnimatable
Also written: x-grid, x-spacingGrid step on x, or auto.
Defaultauto
y-grid-spacingAnimatable
Also written: y-grid, y-spacingGrid step on y, or auto.
Defaultauto
x-axis-label
Also written: x-labelX axis name.
y-axis-label
Also written: y-labelY axis name.
Only withargandplane
re-domainAnimatable
Also written: re-rangeThe real axis's range on the argand plane, e.g. [-4, 14]. Derived from the data when omitted; animatable with !cue.to and !cue.zoom like x-domain.
im-domainAnimatable
Also written: im-rangeThe imaginary axis's range on the argand plane. Derived from the data when omitted; animatable like y-domain.
re-grid-spacingAnimatable
Also written: re-grid, re-spacingGrid step on the real axis, or auto.
Defaultauto
im-grid-spacingAnimatable
Also written: im-grid, im-spacingGrid step on the imaginary axis, or auto.
Defaultauto
re-axis-label
Also written: re-labelThe real axis's name — Re unless given.
im-axis-label
Also written: im-labelThe imaginary axis's name — Im unless given.
Only withpolarplane
r-domainAnimatable
Also written: r-rangeThe polar plane's radius, e.g. [0, 3]: the outermost ring. Derived from the marks when omitted, snapped up to the r ladder. Under r scale: log the start is the radius at the centre.
theta-domainAnimatable
Also written: theta-rangeThe polar plane's sector, in radians — [0, pi/2] draws a quarter turn. The full turn when omitted. The window an r(theta) curve runs over.
theta-formatAnimatable
Also written: theta-labelsWhat the polar plane's angle axis holds: radians (the default — rays at π/6, labels as fractions of π), degrees (rays every 30°, labelled 30°), or a series of names (North, East, South, West, weekdays, a column) for a radar chart's spokes or a rose's sectors, standing at equal angles round the turn.
Defaultauto
r-scale
How the polar plane's radius is placed: linear or log/log2, which rings at the decades from the r-domain's start outward.
Defaultlinear
r-grid-spacingAnimatable
Also written: r-grid, r-spacingThe ring spacing, or auto.
Defaultauto
theta-grid-spacingAnimatable
Also written: theta-grid, theta-spacingThe ray spacing in radians, or auto (π/6; one per name on a categorical theta).
Defaultauto

#Allowed content

#Allowed in

#Examples

canvas.graph#fit-graph{
    scatter#observations(x: 1, 2, 3, 4; y: 2.0, 2.4, 3.1, 4.8; colour: grey)
    fit#trend(of: #observations; degree: 1 to 4; default: 1; colour: teal)
    cue{
        residuals(of: #trend; colour: orange)
    }(
        in: 1
    )
}(
    x-axis-label: x
    y-axis-label: y
)

#Notes

  • An environment's own id is written on the node — canvas.graph#projectile{} — and continue: points at it with #projectile.