graph

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.

graphNode
A graph of four grey readings with a teal line fitted through them, on axes labelled x and y.A graph of four grey readings with a teal line fitted through them, on axes labelled x and y.

#The graph environment

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 — 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.

Two more the frame carries. aspect: square takes the largest square the margins leave and centres the plot in it — a square frame, not a square unit. Leaving both domains off already gives a picture whose x unit and y unit are the same length on screen, which is right where both axes carry the same thing and wrong where they do not: a quantile-quantile plot is read by whether its points lie along the 45° line, and only a square frame makes that fair, while its two axes are a sample against a distribution and must keep their own scales. Under aspect: square they do, and the two never share a grid step. And seed is the one number every settled pseudo-random thing the graph draws comes from — today the offsets of a scatter(layout: jitter). Which noise a jitter uses is arbitrary; that it is the same noise on every render, in every browser and in three years is not, so the offsets are a function of the seed and the reading's own row and of nothing else. Change the seed and the same sample is arranged differently, which is how a cloud that happens to hide a point is fixed without touching the data.

#Planes

A graph's head names its plane — what a coordinate on its marks is. graph:xy is the ordinary plane and the default; graph:x a number line, with one axis and marks that take x alone; 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; 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.

The complex plane: a vector to 3 + 2i, the point 5 + 12i, and a circle of radius √13 about the origin.The complex plane: a vector to 3 + 2i, the point 5 + 12i, and a circle of radius √13 about the origin.
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]
)
The polar plane: the cardioid r = 1 + cos θ, the point r = 2 at 60°, a band between r = 1 and r = 2, and a band from θ = 0 to π/4.The polar plane: the cardioid r = 1 + cos θ, the point r = 2 at 60°, a band between r = 1 and r = 2, and a band from θ = 0 to π/4.
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. Each mark's page says where it is drawn. 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.

#Axes ruled in discrete numbers

x format: discrete is the same axis in numbers. The values its marks give are sorted, de-duplicated, and stood at 1…n one apiece: five loads of 2, 3, 4, 5 and 6 kN get five stops, and so do four speeds of 800, 1200, 1600 and 2000 rpm. The stops are evenly spaced however far apart the numbers themselves are, each tick writes the value it stands for, and there is nothing between two stops — no tick at a round thousand that falls between two of the speeds actually run, and no room for a reading nobody took.

That is the right axis whenever the numbers are settings rather than measurements: the levels of a factorial experiment, a die's six faces, the years a census was taken. It is the wrong one when the gaps carry meaning — bins of unequal width, readings at irregular times — because it throws the spacing away. numbers is the axis for those, and the difference is worth deciding rather than inheriting: a value the axis was not ruled at is a compile error on a discrete axis, exactly as an unknown name is on a categorical one.

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; an axis carrying nothing but heatmap cells rules itself discrete at their values, since a cell exists at its own setting and nowhere else; 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, 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.

Readings at x = 10, 100 and 1000 against y = 3, 9 and 27 on log–log axes, lying on their fitted straight line.Readings at x = 10, 100 and 1000 against y = 3, 9 and 27 on log–log axes, lying on their fitted straight line.
graph{
    scatter#readings(x: 10, 100, 1000; y: 3, 9, 27)
    fit(of: #readings; degree: 1)
}(
    x scale: log
    y scale: log
)
Revenue in £k for North, South, East and West: grey bars for 2023 and blue bars for 2024.Revenue in £k for North, South, East and West: grey bars for 2023 and blue bars for 2024.
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)
)

#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). Every outline takes dashed, which is how a figure draws the shape that is not there. Each mark's own page opens with what it draws and how to write it; by what they draw, they are:

  • Positions and figures — point, line, segment, vector, polygon, and the shapes, which differ only in outline.
  • Notation — angle marks and measures the angle at a vertex, brace spans a distance and names it, and text sets words anywhere in the picture.
  • Functions — curve plots an expression or a position in t; integral shades the area under one; contour and surface draw a function of x and y, traced and shaded; field draws a differential equation as arrows, with the solutions through chosen points.
  • Readings — scatter and lollipop draw readings at positions, bar stands one at each name, and histogram bins a sample on a numeric axis; heatmap lays a value out in a grid of cells, which is what a correlation matrix is. Every one binds a dataset column directly; the @data page covers where columns come from.
  • Summaries of a sample — boxplot, violin and density take the readings themselves and work the summary out.
  • Models — fit lays a least-squares line or polynomial through a scatter, and residuals draws each reading's distance from it.
  • Axis annotations — axis.label names a value on an axis, axis.brace names an interval beside it, axis.band shades one across the plot, and axis.break cuts one out of the axis.

#Where marks stand

Some marks draw a sample rather than a position, and those stand. On the number line a scatter, a boxplot and a violin each take a lane above the line, in source order, the first sitting on the line.

On a plane a mark stands at a place on the other axis. Written with one coordinate — x: and it lies along the x axis, y: and it stands up the y axis — its on: says where. A number there stands it at that value; a name stands it at that name and rules the axis in the names its marks stand at, so three samples side by side need no x format: line. The room at a place is the band there — a unit on an axis of names, and on a continuous one the closest two places stand, so nothing at one can overlap anything at another. Everything standing at one place splits that room in source order, as several bar marks split the band at one name; width: is each mark's share of its slot.

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

A box, a violin and a scatter of the same readings are one sample drawn several ways, and share one lane or slot rather than splitting it: the summary lands over the readings with nothing to position by hand, and which is drawn over which is source order and layer. Written with both coordinates and one axis ruled in names, a mark stands at every name at once, each reading at the name in its own row — the shape data usually arrives in, one row per reading and a column saying which group it belongs to. The names are drawn at the edge of the plot whatever the other axis's window holds: an axis ruled in names never crosses at the origin, so a diverging plot centred on zero keeps its names clear of the marks.

#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 — 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: graph:value
Defaultxy
title
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
Show tick labels.
Also written: ticks
Defaulttrue
show-axesAnimatable
Show the axes.
Also written: axes
Defaulttrue
show-backgroundAnimatable
Master switch for the frame (grid, axes, ticks).
Also written: background, show-bg, bg
Defaulttrue
show-gridAnimatable
Draw 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.
Also written: grid
Defaulttrue
grid-styleAnimatable
What 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.
lineslines-minordotsdots-minor
Also written: grid-type
Defaultlines
seed
The number every settled pseudo-random thing this graph draws comes from — today, the offsets of a scatter(layout: jitter). Which noise a jitter uses is arbitrary; that it is the same noise every time is not, since a picture that rearranged itself between visits would have the reader looking for meaning in the rearrangement, and a document whose figures are not the same twice cannot be cited. So each offset is a function of this number and the reading's own row and of nothing else — no clock, no generator carrying state, nothing that differs between one machine and another. Which leaves the seed as an authoring control: the same readings at seed: 2 are the same sample arranged differently, so a jittered cloud that happens to hide a point is fixed by changing one number rather than by editing the data.
Default0
continue
Inherit an earlier same-type environment's state: continue: #that-environment.
Only withxplane
x-domainAnimatable
X 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…).
Also written: domain, x-range
x-formatAnimatable
What 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); discrete, which rules the axis at the NUMBERS its marks give. 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, surface, residuals — is refused. discrete makes it categorical too, in numbers: the values are sorted, de-duplicated and stood at 1…n one apiece — evenly spaced however far apart the numbers themselves are — and ticked at those values and nowhere between them, so a factorial grid of 800, 1200, 1600, 2000 rpm gets four stops rather than a continuous axis ruled at round thousands. A value the axis was not ruled at is a compile error, as an unknown name is. 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, an axis carrying nothing but heatmap cells rules itself discrete at their values, otherwise numbers. A categorical axis is drawn at the plot's edge, the y axis at the left and the x axis at the bottom, never through the origin, so its names stay clear of the marks.
Also written: x-labels
Defaultauto
x-scaleAnimatable
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
Grid step on x, or auto.
Also written: x-grid, x-spacing
Defaultauto
x-axis-label
X axis name.
Also written: x-label
Only withxyplane
x-domainAnimatable
X 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…).
Also written: domain, x-range
y-domainAnimatable
Y 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…).
Also written: range, y-range
x-formatAnimatable
What 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); discrete, which rules the axis at the NUMBERS its marks give. 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, surface, residuals — is refused. discrete makes it categorical too, in numbers: the values are sorted, de-duplicated and stood at 1…n one apiece — evenly spaced however far apart the numbers themselves are — and ticked at those values and nowhere between them, so a factorial grid of 800, 1200, 1600, 2000 rpm gets four stops rather than a continuous axis ruled at round thousands. A value the axis was not ruled at is a compile error, as an unknown name is. 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, an axis carrying nothing but heatmap cells rules itself discrete at their values, otherwise numbers. A categorical axis is drawn at the plot's edge, the y axis at the left and the x axis at the bottom, never through the origin, so its names stay clear of the marks.
Also written: x-labels
Defaultauto
y-formatAnimatable
What the y axis holds — as x-format. A categorical axis is drawn at the plot's edge, the y axis at the left and the x axis at the bottom, never through the origin, so its names stay clear of the marks.
Also written: y-labels
Defaultauto
x-scaleAnimatable
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-scaleAnimatable
How the y axis places a value — as x-scale.
Defaultlinear
x-grid-spacingAnimatable
Grid step on x, or auto.
Also written: x-grid, x-spacing
Defaultauto
y-grid-spacingAnimatable
Grid step on y, or auto.
Also written: y-grid, y-spacing
Defaultauto
x-axis-label
X axis name.
Also written: x-label
y-axis-label
Y axis name.
Also written: y-label
transformAnimatable
The map the plane is drawn through. On xy it is a 2×2 matrix written as its ROWS — (1, 1), (0, 1) is (x, y) ↦ (x + y, y), and its columns are where the basis vectors land; entries may be parametised, (1, k), (0, 1). On argand it is a function of z — z^2, 1/z, (z + 1/z)/2. Every position goes through it and the grid is drawn as the image of the grid; the frame does not move, since the window, its ticks and its numbers measure the space the picture is mapped into. An auto domain is settled once, over the marks and their images under every map the graph will pass through, so the window holds the whole animation from the start. Refused beside a mark that reads an axis rather than a position (bar, histogram, density, boxplot, violin, lollipop, fit, residuals, integral, contour, surface, heatmap) and beside editable:.
aspect
The shape of the plot region: 'auto' fills the room the environment gives it, 'square' takes the largest square that room holds and centres it there. A square FRAME, not a square unit — leaving both domains off already gives a picture whose x unit and y unit are the same length on screen, which is right where the two axes carry the same thing (a symmetry plot, both axes in dollars) and wrong where they do not. A quantile-quantile plot is read by whether its points lie along the 45° line, and that judgement is only fair on a square frame; its axes are a sample against a distribution, so tying their units together would be a claim about the data. Under 'square' each axis goes on framing its own readings, and the two never share a grid step. R's par(pty = "s"). Withdrawn on the number line, which is a band as tall as its lanes need, and on the polar plane, whose disc is already drawn square.
autosquare
Defaultauto
Only withargandplane
re-domainAnimatable
The 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.
Also written: re-range
im-domainAnimatable
The imaginary axis's range on the argand plane. Derived from the data when omitted; animatable like y-domain.
Also written: im-range
re-grid-spacingAnimatable
Grid step on the real axis, or auto.
Also written: re-grid, re-spacing
Defaultauto
im-grid-spacingAnimatable
Grid step on the imaginary axis, or auto.
Also written: im-grid, im-spacing
Defaultauto
re-axis-label
The real axis's name — Re unless given.
Also written: re-label
im-axis-label
The imaginary axis's name — Im unless given.
Also written: im-label
transformAnimatable
The map the plane is drawn through. On xy it is a 2×2 matrix written as its ROWS — (1, 1), (0, 1) is (x, y) ↦ (x + y, y), and its columns are where the basis vectors land; entries may be parametised, (1, k), (0, 1). On argand it is a function of z — z^2, 1/z, (z + 1/z)/2. Every position goes through it and the grid is drawn as the image of the grid; the frame does not move, since the window, its ticks and its numbers measure the space the picture is mapped into. An auto domain is settled once, over the marks and their images under every map the graph will pass through, so the window holds the whole animation from the start. Refused beside a mark that reads an axis rather than a position (bar, histogram, density, boxplot, violin, lollipop, fit, residuals, integral, contour, surface, heatmap) and beside editable:.
aspect
The shape of the plot region: 'auto' fills the room the environment gives it, 'square' takes the largest square that room holds and centres it there. A square FRAME, not a square unit — leaving both domains off already gives a picture whose x unit and y unit are the same length on screen, which is right where the two axes carry the same thing (a symmetry plot, both axes in dollars) and wrong where they do not. A quantile-quantile plot is read by whether its points lie along the 45° line, and that judgement is only fair on a square frame; its axes are a sample against a distribution, so tying their units together would be a claim about the data. Under 'square' each axis goes on framing its own readings, and the two never share a grid step. R's par(pty = "s"). Withdrawn on the number line, which is a band as tall as its lanes need, and on the polar plane, whose disc is already drawn square.
autosquare
Defaultauto
Only withpolarplane
r-domainAnimatable
The 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.
Also written: r-range
theta-domainAnimatable
The 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.
Also written: theta-range
theta-formatAnimatable
What 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.
Also written: theta-labels
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
The ring spacing, or auto.
Also written: r-grid, r-spacing
Defaultauto
theta-grid-spacingAnimatable
The ray spacing in radians, or auto (π/6; one per name on a categorical theta).
Also written: theta-grid, theta-spacing
Defaultauto

#Allowed content

#Allowed in

#Examples

Three frames of the first step: four grey readings and a teal fitted line, then orange residuals fading in from each reading to the line.Three frames of the first step: four grey readings and a teal fitted line, then orange residuals fading in from each reading to the line.
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; colour: teal)
    cue{
        residuals(of: #trend; colour: orange)
    }(
        in: 1
    )
}(
    x-axis-label: x
    y-axis-label: y
)
A number line from 0 to 14 with a teal box plot of nine readings and the readings as grey dots beneath it.A number line from 0 to 14 with a teal box plot of nine readings and the readings as grey dots beneath it.
graph:x{
    boxplot(x: 2, 3, 3, 4, 4, 5, 5, 7, 12; colour: teal)
    scatter(x: 2, 3, 3, 4, 4, 5, 5, 7, 12; colour: grey)
}
The polar plane: a rose of four petals and the unit circle, on a grid of rings and rays.The polar plane: a rose of four petals and the unit circle, on a grid of rings and rays.
graph:polar{
    curve(expression: 2 * cos(2 * theta); colour: blue)
    curve(expression: 1; colour: grey)
}(
    r-domain: [0, 2.2]
)
Revenue by region drawn as bars on a categorical x axis, with the y axis labelled and the plot titled.Revenue by region drawn as bars on a categorical x axis, with the y axis labelled and the plot titled.
graph{
    bar(x: North, South, East, West; y: 42.1, 31.4, 55.0, 27.8; colour: blue)
}(
    title: Revenue, 2024
    y-axis-label: £k
)

#Notes

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

Last updated