Live plots
A plot is a graph that the app draws in a card. Its marks are expressions in one free variable and in named values. Each named value can have a slider. When the learner drags a slider, the app evaluates the expressions again and draws the graph again, at once.
Where a plot goes
Write a plot as a fenced code block with the language plot, inside a ::: card or a ::: figure:
::: card
Drag the levers. The line is $I = V / R$ for the resistance you choose. Raise $R$ and the line
tilts flatter, so the same voltage produces less current.
```plot
x: { var: V, label: "$V$ in volts", from: 0, to: 12, ticks: 2, grid: true }
y: { label: "$I$ in amperes", from: 0, to: 6 }
inputs:
- { name: R, min: 2, max: 12, default: 4, step: 1, label: resistance R }
draw:
- curve: { is: V / R, accent: true }
```
:::
- The app draws the plot at the end of the card, after its prose, wherever the fence sits in the card.
- A card or a figure draws one plot. A second fence in one card is an error.
- A plot in a flashcard, a question, an answer, a solution, a reference, a derivation or the instructions of an exam is an error, because the app never draws it. Put plots in cards.
The shape
The body of the fence is YAML with five keys:
| Key | Required | Is |
|---|---|---|
x | yes | the horizontal axis, with the free variable |
y | yes | the vertical axis |
inputs | no | the sliders, each a named value |
let | no | named expressions, in order |
draw | no | the marks, back to front |
A plot without inputs has no sliders and does not move. That is all a still graph needs.
Limits
The app draws a plot in the browser of the learner, so the checker holds each plot to these limits:
- Each number is finite:
from,to,ticks,min,max,default,step, and each value of apointsmark..nanand.infare errors. stepandticksare above zero.- A plot has 16 inputs, 32
letentries and 32 marks at most. - A plot has 1000 data points at most, over all of its
pointsmarks. - An expression is 512 bytes at most. Give its parts names in
let.
An axis
x: { var: t, label: "$t$ in seconds", from: 0, to: 10, scale: linear, ticks: 2, grid: true }
| Key | Required | Is |
|---|---|---|
var | no | the name of the free variable. On x only. Without it, the name is x. |
label | no | the text of the axis |
from, to | yes | the extent of the axis, as two numbers |
scale | no | linear (the default) or log |
ticks | no | the distance between two ticks. Without it, the axis has no ticks. |
grid | no | true draws faint lines at the ticks |
The app draws the axes as an L: the vertical axis at from of x, and the horizontal axis at from of y. A tick is a short line. The app does not write numbers at the ticks, so say the scale in the label or in the prose.
On a log axis, from and to must be above zero. Any value of ticks above zero gives one tick at each power of ten. A log axis makes a Bode plot possible.
Labels
A label takes $math$, but SVG cannot hold MathML. So the app removes the dollar signs and \text, writes ten Greek commands as letters, removes the braces and every other backslash, and sets the text in italic:
| Written | Shows |
|---|---|
"$t$ in seconds" | t in seconds |
"$\omega$ in rad/s" | ω in rad/s |
"$V_1$" | V_1 |
The ten Greek commands are \alpha, \beta, \gamma, \delta, \theta, \lambda, \mu, \pi, \tau and \omega. A subscript, a fraction or any other command does not show as math. Keep labels to a symbol and a unit.
Put a label that holds $ or : in double quotes, so that YAML reads it as text.
An input
inputs:
- { name: f, min: 0.5, max: 3, default: 1, step: 0.1, label: frequency }
| Key | Required | Is |
|---|---|---|
name | yes | the name that the expressions use |
min, max | yes | the range of the slider |
default | yes | the value at the start |
step | no | the step of the slider; 1 if absent |
label | no | the text beside the slider; the name if absent |
The slider shows its value with two decimals. Set step to a value below 1 for a smooth slider: with the default step of 1, a slider from 0 to 2 has three positions.
let
let:
w: 2 * pi() * f
env: exp(-d * t)
Each entry names an expression. The app evaluates the entries in the order you write them. Each entry can use the entries before it, the inputs and the free variable. Every mark can use all of them. So say a thing once, and use its name.
The marks
draw is a list. Each item is one mark, and the app draws them in order, so the first item is at the back.
| Mark | Draws | Keys |
|---|---|---|
curve | y = f(x) | is, over |
param | a parametric pair (x(s), y(s)) | var, over (required), x, y |
polar | r(θ), with θ in radians | var, over (required), r |
point | one point, with an optional label | at: [x, y] |
points | a series of data points | a list of [x, y] pairs |
hline | a horizontal line | at |
vline | a vertical line | at |
area | a filled region | under or between, over |
draw:
- curve: sin(w * t)
- curve: { is: env, accent: true, dash: true, label: envelope }
- curve: { is: 1 / t, over: [0.5, 10] }
- param: { var: s, over: [0, 2 * pi()], x: cos(s), y: sin(s) }
- polar: { var: a, over: [0, 2 * pi()], r: 1 + cos(a) }
- point: { at: [2, 1], label: "$t_0$" }
- points: [[0, 0], [1, 0.8], [2, 1.4]]
- hline: { at: 0.707, label: "$-3$ dB", dash: true }
- vline: { at: 2.5 }
- area: { under: sin(w * t), over: [0, pi()] }
- area: { between: [sin(t), cos(t)], over: [0, 1] }
Three marks have a short form, with the expression alone:
- curve: a * x + b
- hline: 0.5
- vline: 2.5
A points series can be a bare list, as above, or a map: points: { at: [[0, 0], [1, 0.8]] }.
over
over is the extent a mark covers, as [from, to]. Both ends are expressions, so an extent can follow a slider: over: [0, 1 / (2 * f)]. Without over, a curve and an area cover the whole x axis. A param and a polar need over.
Two curves with adjoining over ranges draw a function in pieces, for example a square wave or a diode curve.
Details of the marks
- A
pointat[x, y]readsyat its ownx: the free variable takes the value ofx. Soat: [1 / (4 * f), env]is a point on the curveenv, and it moves with the sliders. - The
xandyof aparam, and therof apolar, use the variable thatvarnames. Withoutvar, it iss. - A
pointsseries takes plain numbers, not expressions, because it is data. - An
areawithunderfills between the curve and the liney = 0. If theyaxis starts above zero, it fills down to the bottom of the axis. - The app draws each mark from 97 samples across its extent. On a
logaxis, the samples are even in the logarithm.
Style
Three style keys are written the same way on every mark, and every mark draws all three, points and area included:
| Key | Gives |
|---|---|
accent: true | the accent colour. A point and a points series always take it. |
dash: true | a dashed stroke |
label: text | a text beside the mark. On a curve, it sits at the right end. |
accent gives the one colour that a figure can carry beside the ink. Use it for the one thing the learner must watch.
The expression language
The app evaluates each expression with fasteval, plus three functions of its own.
| Kind | Items |
|---|---|
| numbers | 2, 0.5, -3, 1e-3 |
| arithmetic | +, -, *, /, %, ^ |
| comparisons | <, <=, ==, !=, >=, >, which give 1 or 0 |
| constants | pi(), e() |
| trigonometry | sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh |
| other functions | abs, sign, min(a, b, …), max(a, b, …), floor, ceil, round, int, log |
| Stemin's own | exp(x), sqrt(x), if(c, a, b) |
The logic operators are && (and), || (or) and ! (not).
Take care with these:
piandeare functions. Writepi()ande(), with the brackets.log(x)is the logarithm in base 10.log(b, x)takes the base first. The natural logarithm islog(e(), x).round(x)rounds to a whole number.round(m, x)rounds to a multiple ofm.if(c, a, b)givesawhencis not zero, andbwhen it is. A comparison gives1or0, soif(t < 0, -1, 1)is a step.- Angles are in radians.
- Always write
*between a number and a name. A number takes a unit suffix, so2kis 2000,3mis 0.003, and5nis 0.000000005. Write2 * k.
An expression can use the free variable, every input, every let name before it, and the var of its own param or polar.
A bad expression draws nothing, with no message. An unknown name or a missing bracket gives no value. A value that is not finite gives no value either, for example a division by zero or the root of a negative number. A curve then has a gap at that sample. A point or a line with no value does not show. The checker does not evaluate expressions. It only holds each one to 512 bytes. So open the card in the app and move each slider across its range.
What the checker reads
The checker finds a plot where the app does not draw it, and a second plot in one card:
ERROR ./math/alg/linears/solving.md:8: a `plot` block draws only in a `card` or a `figure`, and this one sits in `flashcard`, so the app never draws it: move it to a card
ERROR ./math/alg/linears/solving.md:11: this `card` already holds a `plot` block, and a card draws only one: put the second plot in a card of its own
It reads the YAML of each plot fence. It finds a missing key, a mark that does not exist, two marks in one list item, and a plot past a limit:
ERROR ./math/alg/linears/solving.md:24: this `plot` block will not parse, so it would draw nothing: x: missing field `from` at line 1 column 4
ERROR ./math/alg/linears/solving.md:24: this `plot` block will not parse, so it would draw nothing: draw[0]: unknown variant `wiggle`, expected one of `curve`, `param`, `polar`, `point`, `points`, `hline`, `vline`, `area` at line 4 column 5
ERROR ./math/alg/linears/solving.md:24: this `plot` block will not parse, so it would draw nothing: draw[0]: one mark per list item: start the second with its own `-` at line 4 column 5
ERROR ./math/alg/linears/solving.md:24: this `plot` block will not parse, so it would draw nothing: `x.ticks` is `0`, and a spacing must be above zero: write one like `0.1`
The line after the file is the line of the fence. The line in the message is the line inside the fence.
A worked example
A damped wave, with two sliders:
x: { var: t, label: "$t$ in seconds", from: 0, to: 10, ticks: 2, grid: true }
y: { label: "$V$", from: -1.2, to: 1.2 }
inputs:
- { name: f, min: 0.2, max: 2, default: 0.6, step: 0.1, label: frequency }
- { name: d, min: 0, max: 0.5, default: 0.2, step: 0.05, label: damping }
let:
w: 2 * pi() * f
env: exp(-d * t)
draw:
- area: { under: env * sin(w * t), over: [0, 1 / (2 * f)] }
- curve: env * sin(w * t)
- curve: { is: env, accent: true, dash: true, label: envelope }
- curve: { is: -env, accent: true, dash: true }
- hline: { at: 0 }
- point: { at: [1 / (4 * f), env], label: first peak }
It draws the wave, its envelope above and below, the first half cycle filled, and a labelled point on the first peak. Move f and the point and the filled region move with it, because their coordinates are expressions.
What a plot does not do
| Not this | Do this |
|---|---|
an implicit relation, such as x^2 + y^2 = 9 | a param: a circle is x: 3 * cos(s), y: 3 * sin(s) |
| derivatives and integrals as operators | write the result as an expression |
| regressions and statistics | compute them, and draw points and a curve |
| points that the learner drags | a slider for each value |