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:

KeyRequiredIs
xyesthe horizontal axis, with the free variable
yyesthe vertical axis
inputsnothe sliders, each a named value
letnonamed expressions, in order
drawnothe 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 a points mark. .nan and .inf are errors.
  • step and ticks are above zero.
  • A plot has 16 inputs, 32 let entries and 32 marks at most.
  • A plot has 1000 data points at most, over all of its points marks.
  • 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 }
KeyRequiredIs
varnothe name of the free variable. On x only. Without it, the name is x.
labelnothe text of the axis
from, toyesthe extent of the axis, as two numbers
scalenolinear (the default) or log
ticksnothe distance between two ticks. Without it, the axis has no ticks.
gridnotrue 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:

WrittenShows
"$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 }
KeyRequiredIs
nameyesthe name that the expressions use
min, maxyesthe range of the slider
defaultyesthe value at the start
stepnothe step of the slider; 1 if absent
labelnothe 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.

MarkDrawsKeys
curvey = f(x)is, over
parama parametric pair (x(s), y(s))var, over (required), x, y
polarr(θ), with θ in radiansvar, over (required), r
pointone point, with an optional labelat: [x, y]
pointsa series of data pointsa list of [x, y] pairs
hlinea horizontal lineat
vlinea vertical lineat
areaa filled regionunder 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 point at [x, y] reads y at its own x: the free variable takes the value of x. So at: [1 / (4 * f), env] is a point on the curve env, and it moves with the sliders.
  • The x and y of a param, and the r of a polar, use the variable that var names. Without var, it is s.
  • A points series takes plain numbers, not expressions, because it is data.
  • An area with under fills between the curve and the line y = 0. If the y axis 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 log axis, 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:

KeyGives
accent: truethe accent colour. A point and a points series always take it.
dash: truea dashed stroke
label: texta 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.

KindItems
numbers2, 0.5, -3, 1e-3
arithmetic+, -, *, /, %, ^
comparisons<, <=, ==, !=, >=, >, which give 1 or 0
constantspi(), e()
trigonometrysin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh
other functionsabs, sign, min(a, b, …), max(a, b, …), floor, ceil, round, int, log
Stemin's ownexp(x), sqrt(x), if(c, a, b)

The logic operators are && (and), || (or) and ! (not).

Take care with these:

  • pi and e are functions. Write pi() and e(), with the brackets.
  • log(x) is the logarithm in base 10. log(b, x) takes the base first. The natural logarithm is log(e(), x).
  • round(x) rounds to a whole number. round(m, x) rounds to a multiple of m.
  • if(c, a, b) gives a when c is not zero, and b when it is. A comparison gives 1 or 0, so if(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, so 2k is 2000, 3m is 0.003, and 5n is 0.000000005. Write 2 * 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 thisDo this
an implicit relation, such as x^2 + y^2 = 9a param: a circle is x: 3 * cos(s), y: 3 * sin(s)
derivatives and integrals as operatorswrite the result as an expression
regressions and statisticscompute them, and draw points and a curve
points that the learner dragsa slider for each value