# equation.io > Free in-browser graphing calculator (WebGL). Plots 2D curves, inequality > regions, scalar fields, vector fields and ODEs, complex functions (including > domain coloring, conformal maps and escape-time fractals), 3D surfaces, > parametric curves/surfaces, plane geometry (named draggable points, > segments, polylines, vector arrows, polygons, squares, circles), symbolic > derivatives and integrals, > and random variables with shaded probabilities. No account, no server round-trips: the graph state > lives in the URL fragment, so you can construct a link that opens the app > with any set of equations already rendered. ## Deep links Format: https://equation.io/g/;;... - Percent-encode each equation (JavaScript `encodeURIComponent`), then additionally escape ( ) ! ' * as %28 %29 %21 %27 %2A — chat apps cut auto-linked URLs at bare parentheses, silently truncating the graph. - Encoded equations are joined with `;`, and that literal `;` is only ever the row separator (a reader also accepts `%3B` there, since clients re-encode it). A `;` that belongs to an equation — inside quoted text, like `p[p.city == "a;b"]` or `open("sales;2026.csv")` — is encoded TWICE, as `%253B`, so the two can never be confused: the payload encodes the quotes too, and a singly-encoded one comes back as a row separator, splitting the equation in half. - Every equation gets its own row and color. Definitions (constants, functions, coordinate fields) are rows too — order does not matter. - `/g/` links unfurl in chat apps and social cards with the equations as the title. An equivalent fragment form, `https://equation.io/#`, also opens the graph and keeps the equations out of server logs, but its card is the generic site card. Examples (paste-ready): - [Parabola](https://equation.io/#y%3Dx%5E2): `y = x^2` - [Circle](https://equation.io/#x%5E2%2By%5E2%3D4): `x^2 + y^2 = 4` - [Cubic + tangent line](https://equation.io/#f(x)%3Dx%5E3-2x;g(x)%3Dd%2Fdx%20f(x);a%3D1;y%3Df(x);y%3Df(a)%2Bg(a)(x-a)): `f(x) = x^3 - 2x; g(x) = d/dx f(x); a = 1; y = f(x); y = f(a) + g(a)(x-a)` - [Slider-controlled wave](https://equation.io/#a%3D2;y%3Dsin(a%20x)%2Fa): `a = 2; y = sin(a x)/a` - [Fourier series](https://equation.io/#N%20%3D%208;y%20%3D%202%20sum%5Bn%3D1..N%5D%20(-1)%5E(n%2B1)%20sin(n%20x)%2Fn): `N = 8; y = 2 sum[n=1..N] (-1)^(n+1) sin(n x)/n` — drag N for more harmonics - [Animated ripples](https://equation.io/#sin(x%5E2%2By%5E2-4t)%2F2): `sin(x^2+y^2-4t)/2` - [3D saddle](https://equation.io/#z%20%3D%20x%5E2%20-%20y%5E2): `z = x^2 - y^2` - [Torus (parametric 3D)](https://equation.io/#(cos(2pi%20u)(2%2Bcos(2pi%20v))%2C%20sin(2pi%20u)(2%2Bcos(2pi%20v))%2C%20sin(2pi%20v))): `(cos(2pi u)(2+cos(2pi v)), sin(2pi u)(2+cos(2pi v)), sin(2pi v))` - [Complex dipole field](https://equation.io/#ln(w-2)-ln(w%2B2)): `ln(w-2) - ln(w+2)` - [Domain coloring](https://equation.io/#domain((w%5E3%20-%201)%2Fw)): `domain((w^3 - 1)/w)` - [Conformal grid image](https://equation.io/#conformal(w%5E2%2F4)): `conformal(w^2/4)` - [Mandelbrot set](https://equation.io/#iter(z%5E2%20%2B%20w)): `iter(z^2 + w)` - [Julia set](https://equation.io/#iter(z%5E2%20-%200.7269%20%2B%200.1889i)): `iter(z^2 - 0.7269 + 0.1889i)` - [Koch snowflake (recursive function)](https://equation.io/#f(z)%20%3D%20%7Bre(z)%20%3E%3D%201%3A%201%2C%20f(4%20-%203(z%5E6)%5E(1%2F6))%7D;f(x%20i%20-%20%7Cy%7C)%20%3E%3D%200): `f(z) = {re(z) >= 1: 1, f(4 - 3(z^6)^(1/6))}; f(x i - |y|) >= 0` — a tail-recursive function shades where its recursion terminates - [Vector field](https://equation.io/#(-y%2C%20x)): `(-y, x)` — drawn as animated streamlines - [Pendulum phase portrait](https://equation.io/#(x'%2C%20y')%20%3D%20(y%2C%20-sin(x))): `(x', y') = (y, -sin(x))` - [Slope field](https://equation.io/#y'%20%3D%20x%20-%20y): `y' = x - y` - [Polar cardioid](https://equation.io/#r%3Dsqrt(x%5E2%2By%5E2);theta%3Datan2(y%2Cx);r%3D2(1%2Bcos(theta))): `r = sqrt(x^2+y^2); theta = atan2(y,x); r = 2(1 + cos(theta))` - [Circle through a draggable point](https://equation.io/#C%20%3D%20(0%2C%200);P%20%3D%20(2%2C%201);circle(C%2C%20%7CP%20-%20C%7C);segment(C%2C%20P)): `C = (0, 0); P = (2, 1); circle(C, |P - C|); segment(C, P)` - [Triangle: a side and its angles](https://equation.io/#A%20%3D%20(-2%2C%20-1);B%20%3D%20(3%2C%20-0.5);C%20%3D%20(0.5%2C%202.5);polygon(A%2C%20B%2C%20C);distance(A%2C%20B);angle(B%2C%20A%2C%20C)%20180%2Fpi;angle(B%2C%20A%2C%20C)%20%2B%20angle(C%2C%20B%2C%20A)%20%2B%20angle(A%2C%20C%2C%20B)): `A = (-2, -1); B = (3, -0.5); C = (0.5, 2.5); polygon(A, B, C); distance(A, B); angle(B, A, C) 180/pi; angle(B, A, C) + angle(C, B, A) + angle(A, C, B)` — distance() and angle() are numbers, so the bare rows read out "= value" and follow the points as you drag; the angle sum stays π (−π once the triangle turns clockwise: angles are signed) - [Vector sum (parallelogram rule)](https://equation.io/#A%20%3D%20(3%2C%201);B%20%3D%20(1%2C%202);vector(A);vector(B);vector(A%20%2B%20B);polyline(A%2C%20A%20%2B%20B%2C%20B)): `A = (3, 1); B = (1, 2); vector(A); vector(B); vector(A + B); polyline(A, A + B, B)` — vector() draws arrows (from the origin with one argument), polyline() the open path completing the parallelogram; drag A or B - [Thébault's theorem (squares on a parallelogram)](https://equation.io/#A%20%3D%20(0%2C%200);B%20%3D%20(4%2C%200.5);D%20%3D%20(1%2C%202.5);C%20%3D%20B%20%2B%20D%20-%20A;polygon(A%2C%20B%2C%20C%2C%20D);square(B%2C%20A);square(C%2C%20B);square(D%2C%20C);square(A%2C%20D);P%20%3D%20midpoint(A%2C%20B)%20-%20perp(B%20-%20A)%2F2;Q%20%3D%20midpoint(B%2C%20C)%20-%20perp(C%20-%20B)%2F2;R%20%3D%20midpoint(C%2C%20D)%20-%20perp(D%20-%20C)%2F2;S%20%3D%20midpoint(D%2C%20A)%20-%20perp(A%20-%20D)%2F2;polygon(P%2C%20Q%2C%20R%2C%20S)): `A = (0, 0); B = (4, 0.5); D = (1, 2.5); C = B + D - A; polygon(A, B, C, D); square(B, A); square(C, B); square(D, C); square(A, D); P = midpoint(A, B) - perp(B - A)/2; …; polygon(P, Q, R, S)` — drag A, B or D; the square centers stay a square - [Normal probability](https://equation.io/#m%20%3D%201;s%20%3D%200.5;X%20~%20Normal(m%2C%20s);P(0%20%3C%20X%20%3C%202)): `m = 1; s = 0.5; X ~ Normal(m, s); P(0 < X < 2)` — the density with the area between 0 and 2 shaded; drag m or s and the probability follows - [Swinging pendulum (integrated state)](https://equation.io/#th'%20%3D%20om;om'%20%3D%20-sin(th)%20-%20om%2F8;th(0)%20%3D%203;bob%20%3D%20(sin(th)%2C%20-cos(th));segment((0%2C%200)%2C%20bob);bob): `th' = om; om' = -sin(th) - om/8; th(0) = 3; bob = (sin(th), -cos(th)); segment((0, 0), bob); bob` — th is the angle, om the angular velocity; segment() draws the rod, the named point draws the bob - [Double pendulum](https://equation.io/#g%20%3D%209.8;L1%20%3D%201;L2%20%3D%201;m1%20%3D%201;m2%20%3D%201;M%20%3D%20(((m1%2Bm2)%20L1%2C%20m2%20L2%20cos(th_1%20-%20th_2))%2C%20(L1%20cos(th_1%20-%20th_2)%2C%20L2));f%20%3D%20(-m2%20L2%20om_2%5E2%20sin(th_1%20-%20th_2)%20-%20(m1%2Bm2)%20g%20sin(th_1)%2C%20L1%20om_1%5E2%20sin(th_1%20-%20th_2)%20-%20g%20sin(th_2));th'%20%3D%20om;om'%20%3D%20solve(M%2C%20f);th(0)%20%3D%20(2.5%2C%202.4);b1%20%3D%20(L1%20sin(th_1)%2C%20-L1%20cos(th_1));b2%20%3D%20b1%20%2B%20(L2%20sin(th_2)%2C%20-L2%20cos(th_2));segment((0%2C%200)%2C%20b1);segment(b1%2C%20b2);b1;b2): `M = (((m1+m2) L1, m2 L2 cos(th_1 - th_2)), (L1 cos(th_1 - th_2), L2)); f = (…); th' = om; om' = solve(M, f); th(0) = (2.5, 2.4); …` — the Lagrangian form M(θ)ω′ = f: th and om are 2-vector states with components th_1, th_2; solve() is Cramer's rule; segment()s between named bob points draw the linkage - [Orbit (vector gravity)](https://equation.io/#r'%20%3D%20vel;vel'%20%3D%20-9%20r%2F%7Cr%7C%5E3;r(0)%20%3D%20(2%2C%200);vel(0)%20%3D%20(0%2C%201.5);segment((0%2C%200)%2C%20r);r;(0%2C%200)): `r' = vel; vel' = -9 r/|r|^3; r(0) = (2, 0); vel(0) = (0, 1.5); segment((0, 0), r); r; (0, 0)` — vector states with point arithmetic; the bare state name draws as a moving point - [Rössler attractor (state family + orbit)](https://equation.io/#a%20%3D%200.2;b%20%3D%200.2;c%20%3D%205.7;k%20%3D%204;p'%20%3D%20k%20(-p_2%20-%20p_3%2C%20p_1%20%2B%20a%20p_2%2C%20b%20%2B%20p_3%20(p_1%20-%20c));s%20%3D%202pi%20sort(%5B0..299%5D)%2F300;p(0)%20%3D%20(8cos(s)%2C%208sin(s)%2C%200);p%5B1%5D(10..100);p;camera(-0.9%2C%200.35%2C%2045%2C%20(0%2C%200%2C%207))): `p' = k (-p_2 - p_3, p_1 + a p_2, b + p_3 (p_1 - c)); s = 2pi sort([0..299])/300; p(0) = (8cos(s), 8sin(s), 0); p[1](10..100); p` — a tuple of starting values runs 300 copies of the system, in order; `p[1](10..100)` draws one run's path over that time as the attractor, and `p` the 300 particles flowing along it; k speeds the flow up - [Matrix phase portrait](https://equation.io/#a%20%3D%20-1;b%20%3D%20-1%2F4;A%20%3D%20((0%2C%201)%2C%20(a%2C%20b));(x'%2C%20y')%20%3D%20A%20(x%2C%20y)): `a = -1; b = -1/4; A = ((0, 1), (a, b)); (x', y') = A (x, y)` — a linear system as its literal matrix, sliders in the entries - [Poisson: strict versus non-strict](https://equation.io/#m%20%3D%204;X%20~%20Poisson(m);P(X%20%3C%203);P(X%20%3C%3D%203);P(X%20%3D%203)): `m = 4; X ~ Poisson(m); P(X < 3); P(X <= 3); P(X = 3)` — a discrete variable draws its pmf as stems; each P(…) row highlights exactly the stems it counts, so `<` stops at 2, `<=` takes the stem at 3, and `P(X = 3)` is that one stem - [Two dice](https://equation.io/#X%20~%20DiscreteUniform(1%2C%206);Y%20~%20DiscreteUniform(1%2C%206);S%20%3D%20X%20%2B%20Y;P(S%20%3E%3D%2010);P(X%20%3E%20Y);P(X%20%3E%3D%20Y);E(S)): `X ~ DiscreteUniform(1, 6); Y ~ DiscreteUniform(1, 6); S = X + Y; P(S >= 10); P(X > Y); P(X >= Y); E(S)` — arithmetic over discrete variables is exact: the sum's pmf is the triangle on 2…12, drawn as stems; `P(X > Y)` = 15/36 and `P(X >= Y)` = 21/36 differ by the ties, `P(X = Y)` - [Central limit theorem](https://equation.io/#X1%20~%20Uniform%280%2C%201%29;X2%20~%20Uniform%280%2C%201%29;X3%20~%20Uniform%280%2C%201%29;X4%20~%20Uniform%280%2C%201%29;S%20%3D%20X1%20%2B%20X2%20%2B%20X3%20%2B%20X4;Z%20~%20Normal%282%2C%20sqrt%281%2F3%29%29;P%28S%20%3E%203%29): `X1 ~ Uniform(0, 1); X2 ~ Uniform(0, 1); X3 ~ Uniform(0, 1); X4 ~ Uniform(0, 1); S = X1 + X2 + X3 + X4; Z ~ Normal(2, sqrt(1/3)); P(S > 3)` — the sum of four uniforms hugs the matching normal - [Conditional random variable](https://equation.io/#X%20~%20Normal%280%2C%201%29;Y%20%3D%20%7BX%20%3E%200%3A%20X%5E2%2C%201%7D;P%28Y%20%3E%200.5%29;P%28Y%20%3E%20X%29): `X ~ Normal(0, 1); Y = {X > 0: X^2, 1}; P(Y > 0.5); P(Y > X)` — a piecewise transform of X, with probabilities of derived and joint events ## Share links (/g/) The same payload also works as a path, and this form unfurls with the graph's equations as the card title in chat apps and social cards: https://equation.io/g/;;... Prefer the `/g/` form when the link will be posted somewhere that shows link previews; prefer the `#` form otherwise (it keeps the equations out of server logs). Both open the same app with the same graph. ## MCP server An MCP endpoint (Streamable HTTP) lives at: https://equation.io/mcp Tools: `encode_graph_url` (validates each equation through the app's own parser, returns url/share_url plus per-row classification or errors, with text-only MCP responses) and `decode_graph_url` (decodes an existing link back into its equation rows so you can edit a user's graph and build a new link). This document is also served as the server's `syntax` MCP resource, so the full reference can be pulled on demand instead of living in a tool description. No authentication required. Use `show_graph` to display the interactive graph directly in an MCP Apps host such as ChatGPT web. It accepts the same `equations` array and returns per-row validation and share links plus an associated UI resource. The widget supports editing, sliders, zoom, and 3D rotation; edits update the model's context with the complete current equations and share_url. Preserve those edited rows on follow-up requests. Use `encode_graph_url` for validation or link-only requests. Clients without MCP Apps support can open share_url. ## Expression syntax - Operators: `+ - * / ^`, parentheses, unary minus. Multiplication is implicit: `2x`, `x y`, `2cos(t)`, `a(x-1)` all work. After a `/`, spacing groups: factors written touching stay in the divisor (`1/2pi` is 1/(2π), `x^2/2s^2` is x²/(2s²), `1/x(x+1)` is 1/(x(x+1))), while a space ends it (`g/L sin(th)` is (g/L)·sin(th), `1/2 m v^2` is m v²/2, `(y - b)/h (x - a)` is a slope times (x - a)). `·` and `×` multiply numbers too, and between two vectors are the dot and cross product (see Vector calculus); `⊗` and `∧` are the outer and wedge products (see Tensors). All four bind like `*`. - Absolute value bars: `|x|`, including nested/multiplied forms like `2|x-1|`. - Unicode: Greek letters are ordinary names (`θ = 1` makes a θ slider; `Δx` is a name like `dx`), superscript digits are exponents (`x³` is `x^3`, `x⁻²` is `x^(-2)`), and subscript digits are the `_` subscript syntax in its unicode spelling — `T₀` IS `T_0`, so `r₁` reads a vector state's `r_1` component and `a₃` a sequence's third term. A single name or number in braces is the same subscript: `T_{c}` is `T_c`, `a_{10}` is `a_10`. `·`, `⋅`, `×`, `÷`, `−`, `≤`, `≥`, `≠` mean their ASCII counterparts (`⊗` is `outer(a, b)`, `∧` is `wedge(a, b)`; the editor types them from `\otimes` and `\wedge`). π never joins a longer name, so `2πr` is 2·π·r. In the editor, typing `\pi`, `\theta`, `\nabla`, … inserts the symbol itself, and a `\` before any built-in function name simply drops (`\sin(x)` → `sin(x)`, `\trail` → `trail`), so `\` also works as a function search. - Constants: `pi`, `e` (π also parses directly; τ and `tau` are ordinary names, free to define), and the unit vectors `e_x`, `e_y`, `e_z` = `(1,0,0)`, `(0,1,0)`, `(0,0,1)` (always 3D, so a row using one is a 3D scene; defining `e_x` yourself replaces it). Imaginary unit `i` in complex expressions. (These can be replaced for the whole graph with `:=`; see Reserved names. Prefer another name.) - Functions: sin cos tan asin acos atan atan2(y,x) sinh cosh tanh sech coth asinh acosh atanh sqrt abs exp ln (natural) log (base 10) log2 floor ceil round min max mod sign fract sinc gamma factorial, complex helpers re im arg conj, and point helpers dot cross perp midpoint unit. Also `erf(x)`, `normalpdf(x, mean, sd)` and `normalcdf(x, mean, sd)`; `gcd(a, b)`, the greatest common divisor of whole numbers (other arguments round to the nearest one), so `1/gcd(floor(x), floor(y))` is 1 on the coprime cells of the grid; and `isprime(n)`, 1 for a prime and 0 otherwise (`a_n = isprime(n)`), which gives no value past 2048² − 1 rather than guess. Vector calculus has `grad`, `div`, `curl`, `laplacian`, `jacobian` and `hessian`, and the geometry of a curve `curvature`, `torsion`, `osculating` and `frame` (all below). Function names are case-insensitive: `Sin(x)`, `SQRT(x)` and `sin(x)` are the same call. Always use parentheses for arguments — write `sin(x)`, not `sin x`. - Factorial: postfix `!` — `5!` is 120, `y = x!` plots Γ(x+1), `2^3!` is 2^6. Non-integers use the Gamma function; negative integers are undefined. - Piecewise: `{x < 0: -x, x >= 0: x^2}` — `cond: value` cases tried in order, with an optional last bare value as the default (`{0 < x < 1: 1, 0}`). Conditions are inequalities and may chain. With no default the value is undefined wherever no case holds and nothing is drawn there, which makes one case a domain restriction: `y = {0 < x < 2: x^2}` plots the parabola on (0, 2) only. It works in any row kind — `x^2 + y^2 = {x > 0: 16}` is a half circle, `(2cos(u), {sin(u) > 0: 2sin(u)})` cuts a parametric curve. A bare condition is a case worth 1, as in Desmos: `{x > 0, 5}` is `{x > 0: 1, 5}`. Values may be complex (`{x > 0: w, conj(w)}`); the conditions themselves compare real values (`re(w) > 0`, `|w| < 1`). A condition can be named and reused: `r = x^2 + y^2 + z^2`; `within = r < 16`; `x + y = {within: 1}` — the name reads as the inequality it stands for, in a function body too (its x is the plane's, not the function's parameter), and filters reductions: `count(within)`, `mean({within: x})` (in a function body, a filter whose condition reads a parameter's name is an error: rename the parameter). The definition draws nothing; a row of just the name draws what the inequality would; arithmetic on it is an error. A function may return a condition too: `inside(p) = p < 2`; `y = {inside(x): x^2}`. A name as a bare condition needs a `cond: value` part beside it, `{within, x > 0: 1}`: `{within, 5}` is a tuple, so write `{within: 1, 5}`. - Vector calculus: these operators expand symbolically like d/dx, so sliders and t work inside them. - `grad(f)` is the tuple (∂f/∂x, ∂f/∂y), plus ∂f/∂z when f uses z: `grad(x^2 - y^2)` plots as a vector field. - `div(F)` is ∂F₁/∂x + ∂F₂/∂y (+ ∂F₃/∂z), a scalar field: `div((x y, y^2))`. - `curl(F)` of a 3D field is a vector field: `curl((y z, -x z, 0))`. Of a plane field it is the scalar ∂F₂/∂x − ∂F₁/∂y: `curl((-y, x))` is 2. - `laplacian(f)` is div(grad(f)), for a scalar f: `laplacian(x^2 + y^2)` is 4. - `jacobian(F)` is the matrix of partials, row i the gradient of F's component i (2 components: over x, y; 3: over x, y, z), and `hessian(f)` the matrix of second partials of a scalar f. A 2×2 one in x and y draws as a matrix field: `jacobian((x^2 - y^2, 2 x y))` is all circles, the mark of a conformal map. - With ∇ (in the editor, type `\nabla`): `∇f` is grad(f), `∇·F` is div(F), `∇×F` is curl(F), and `∇²f` (or `∇^2 f`) is laplacian(f). ∇ takes the rest of the product, as d/dx does: `∇ x^2 y` and `∇f(x, y)`. - Names work as arguments. With `f(x, y) = x^2 - y^2`, `grad(f)` and `∇f` mean grad(f(x, y)). With a field `r = sqrt(x^2 + y^2)`, `grad(1/r)` differentiates through r (so does `d/dx r`). With a named vector `F = (-y, x)`, `curl(F)` and `∇×F` work. - The Laplacian of a vector field is not supported. grad of a vector and div or curl of a scalar are errors. - Between two vectors, `A · B` (also `⋅`) is dot(A, B) and `A × B` is cross(A, B). `*` between vectors is an error, since it could mean either. `·` binds like `*`, so `∇f · (1, 1)/sqrt(2)` is a directional derivative. - Fourier analysis: `fourier(signal)` draws a one-sided amplitude spectrum as points (frequency, amplitude). The signal is either a real expression over one interval, which is its variable and its period — `s = interval(0, 1); fourier(sin(2pi s))`, or `fourier(f(s))` for a function f — or ordered, equally spaced real samples (`fourier((0, 1, 0, -1))`). A signal in x (`fourier(sin(2pi x))`) or a bare function name (`fourier(f)`) is an error that shows the interval form: nothing binds x for you. A bracket list is an unordered multiset; give samples as a tuple. For CSV row order, use `S = sort(data.signal, data.row); fourier(S)`; rows must be equally spaced. `sort(data.signal)` sorts by VALUE and changes the signal. - The interval interval(lo, hi) is one period [lo, hi): frequency is in cycles per unit of it, bins k/(hi-lo), from DC through Nyquist. Amplitude is the sinusoid's peak amplitude; DC is the absolute mean, and DC and an even-length Nyquist bin are not doubled. No window is applied. A nonperiodic endpoint mismatch causes spectral leakage. - A signal over an interval uses 256 equally spaced samples; an optional second argument sets a whole sample count from 2 to 4096: `s = interval(0, 2pi); fourier(sin(3s), 512)`. The endpoint hi is excluded. Samples span [0, 1), or the interval given after them: `fourier(S, interval(0, 10))`; they use their own length (2–4096), so give no sample count. Odd lengths and non-powers of two work. - `reconstruct(signal, N)` returns a periodic real expression in x: its signed mean plus frequency bins 1 through N, with phase preserved and the signal's interval as the period. Draw it with `y = reconstruct(f(s), N)`. N is a whole number from 0 to min(128, floor(sample count/2)); N = 0 is the mean. Use a bounded slider such as `N = clamp(round(5), 0, 48)` to add harmonics. The optional sample count, or a tuple's interval, follows N: `y = reconstruct(f(s), N, 512)`. Defaults match fourier. At full available N, the reconstruction interpolates the samples; discontinuities can exhibit Gibbs overshoot. - Coefficients are numeric approximations computed when the document changes. Static sliders in signals or interval bounds recompute them; signals cannot depend on t or simulation states. Missing/nonfinite samples are errors, rather than removed (removing them would change spacing). CSV data stays device-local in shared links, like other data plots. - Name a spectrum to draw stems with existing geometry: `S = fourier(f(s)); segment((S.x, 0), S); S`. Use split views with a shared x axis for the original and reconstruction; the spectrum has its own frequency axis. The Fourier analysis examples demonstrate all three. - Curves: the differential geometry of a parametric curve in u — a tuple `(f(u), g(u))` or `(f(u), g(u), h(u))`, a named one `C = (…)`, or a function of one parameter `f(s) = (…)`. These expand symbolically like d/dx, so sliders and t work inside them. - `curvature(C, u0)` reads out κ at u = u0. In the plane it is signed, positive where the curve turns left (counterclockwise): `curvature((u, u^2), 0)` is 2, and `curvature((2cos(u), -2sin(u)), 0)` is −0.5. In space it is |r′ × r″|/|r′|³ ≥ 0. - `torsion(C, u0)` of a space curve: how fast it twists out of its osculating plane. The helix `C = (2cos(u), 2sin(u), u)` has curvature 0.4 and torsion 0.2 everywhere. A plane curve has none (an error). - Without u0 both are functions of u, for use in other rows: `(u, curvature(C))` plots κ against u, and `k(u) = curvature(C, u)` names it. Alone on a row (or `1/curvature(C)`) they are an error that says so (a row in u alone would draw the density of its values). A list u0 reads each point: `curvature(C, [0.2, 0.4])`. - `osculating(C, u0)` draws the osculating circle at u = u0: through the point, radius 1/|κ|, centred on the concave side. A space curve's lies in its osculating plane. Where κ = 0 (an inflection) it is the tangent line, and near one a large circle that flattens into it. A list u0 draws one circle per point. - `frame(C, u0)` draws the Frenet frame at u = u0 as unit arrows from the point: T (tangent), N and, in space, B = T × N; one point at a time. In the plane N is T turned left (with κ signed, κN points toward the centre of curvature); in space N points toward the centre, and N and B are undefined where κ = 0. With t for u0 it rides along; a `C` row draws u from 0 to 1, so trace the curve once over that and wrap t into it: `C = (2cos(2pi u), sin(2pi u))`, `C`, `frame(C, t/10)`, `osculating(C, t/10)` (u0 past 1 is the same point again). - Surfaces: the curvature of a parametric surface in u and v — a tuple `(f(u, v), g(u, v), h(u, v))`, a named one `S = (…)`, or a function of two parameters. Expanded symbolically from the first and second fundamental forms like `curvature(C)`, so sliders and t work inside them (central differences stand in where a part cannot be differentiated: floor, gamma). - `gaussian(S)` is the Gaussian curvature K = (LN − M²)/(EG − F²), a function of u and v; `gaussian(S, u0, v0)` (or `gaussian(S, (u0, v0))`, `gaussian(S, P)`) reads it at a point. The sphere of radius R has K = 1/R² everywhere; the saddle `(u, v, u^2 - v^2)` has −4 at (0, 0); a torus `((3 + cos(v)) cos(u), (3 + cos(v)) sin(u), sin(v))` has cos(v)/(3 + cos(v)): positive on the outside, negative round the hole. - `meancurvature(S)` is the mean curvature H = (EN − 2FM + GL)/(2(EG − F²)), with the normal ∂S/∂u × ∂S/∂v: H is negative where the surface bends away from that normal, so the sphere `(R cos(v) cos(u), R cos(v) sin(u), R sin(v))`, whose normal points out, has H = −1/R (swap u and v and it is +1/R). Minimal surfaces (catenoid, helicoid, Enneper's) have H = 0. - Alone on a row, `gaussian(S)` (or `meancurvature(S)`) draws S coloured by it on a diverging scale: toward the row's colour where it is positive, its complement where negative, grey at 0, scaled to the surface's typical size of it (so read values with the point form). With `u = interval(0, 2pi)` and `v = interval(0, 2pi)` above, the torus is `S = ((3 + cos(v)) cos(u), (3 + cos(v)) sin(u), sin(v))` and `gaussian(S)`. Elsewhere they are numbers in u and v: `(u, v, gaussian(S))` is K as a height over the parameter square. - On a panel drawn on a surface (an `on(…)` row), `gaussian(x, y)` and `meancurvature(x, y)` are K and H of that surface in the panel's x and y: a scalar field painted on it on the same diverging scale, read on hover; `gaussian(P)` or `gaussian(0.5, 1)` is the number at a point. - `geodesic(S, (u0, v0), (du, dv))` draws the geodesic of S — the straightest curve on it — from the point at (u0, v0) in the direction (du, dv), given in u and v. It is integrated in the background as it is drawn (the last one stays on screen until the next is ready), at unit speed along the surface, from the geodesic equations with the Christoffel symbols of S's metric (expanded symbolically). A fourth argument is its length on the surface (`geodesic(S, P, (1, 0), 5)`; negative runs backwards); without it, it runs twice the size of the surface's bounding box. It stops where it leaves u's and v's ranges (0..1, or their intervals) — except across a range the surface repeats over, like a torus's angles or a sphere's longitude, where it runs on — and where the metric degenerates (a sphere's pole, a cone's apex). The start, direction and length may use sliders, t and named points, so dragging P moves a `geodesic(S, P, …)`; a list draws a fan: `a = [0..11] pi/6` and `geodesic(S, (0, 0), (cos(a), sin(a)))`. It must be the whole row. A fan of very long geodesics shares a fixed amount of work, so its members may end short of the length asked for; the row's note then says so ("cut short at L ≈ 472 of 1000"). On a sphere they are great circles, on a cylinder helices, on a plane straight lines; on a surface of revolution r cos ψ is constant along one (Clairaut). - On a panel drawn on a surface, `geodesic((x0, y0), (dx, dy))` (or `geodesic(P, (dx, dy), L)`) draws on that surface, in the panel's x and y, and runs on across x and y's ranges where the surface repeats. Drag P along the surface to move it. - Derivatives: `d/dx (x^3)` differentiates symbolically; higher order `d^2/dx^2 (...)`; works for any single-letter variable, e.g. `d/dq (q^2)`. The operand is every factor written after `dx`, with or without spaces, up to the next operator or closing bracket: `d/dx x sin(x)` is the derivative of x sin(x), while `d/dx x^3/x` is 3x²/x, `d/dx x * x` is 1·x, and `(d/dx x^2) x` is 2x·x. Along x, y, or z it works through lists too: with `N = [1..4]`, `y = d/dx x^N` is a family of curves, and `y = d/dx [sin(x), x^2]` draws `y = cos(x)` and `y = 2x`. Along t a named list is refused (its elements may use t). - Sums and products: `sum(n=1..N, sin(n x)/n)` and `prod(k=1..N, k)`, also written `Σ`/`Π`. The bracket form `sum[n=1..N] sin(n x)/n` takes everything after it in the product chain as the body (`2 sum[n=1..N] ...` keeps the 2 outside). Bounds must be numbers or constants defined in another row (not `t`), because the sum is expanded symbolically before compiling — which makes `N` a slider that adds terms as you drag it. A bound may also be a named list of numbers: with `N = [3..5]`, `y = sum(n=1..N, sin(n x)/n)` draws one partial sum per element (a family that pairs with other uses of `N`). Inside a sum over `n`, a sequence term `a_n` is each term: `sum(n=1..N, a_n)`. - Integrals: `int[a..b] f(x) dx` (definite) and `int(f(x) dx)` (antiderivative), also written `∫`. The trailing `dx` names the variable; any letter works, so `y = int[0..x] exp(-t^2) dt` plots a function of x, and bounds may use sliders, t, or x. Integration is symbolic where a verified antiderivative exists — polynomials and rational functions (exactly), the elementary families, u-substitution, integration by parts, Gaussians via erf — and falls back to Gauss–Legendre quadrature expanded in place otherwise, so non-elementary rows like `y = int[0..x] sin(t)/t dt` (Si) still plot. A row whose integral resolves to a constant shows the numeric value as a readout — and when the row is exactly one definite integral of a real integrand (`int[0..b] sin(x) dx`; bounds may be sliders, states, t or `±inf`), it also shades the area between the integrand and the axis over [a, b], in 2D: parts that add to the value in the row color, parts that subtract in its complement (so reversed bounds swap the tints). The dx variable runs along the horizontal axis whatever its letter; add `y = sin(x)` as its own row to see the curve beyond [a, b]. A coefficient, a sum of integrals, an iterated integral or a definition (`A = int[0..1] x^2 dx`) stays a plain number. Classic notations work: `int[0..1] dx/(1+x^2)`, and iterated integrals pair inside-out: `int[0..1] int[0..y] x dx dy`. Bounds may be `±inf` (or `∞`): `int[-inf..x] exp(t) dt` is exp(x), `int[-inf..inf] exp(-x^2) dx` reads √π, and `int[-inf..x] normalpdf(t, 0, 1) dt` is the normal cdf. Divergent limits are refused (the closed form is checked against real quadrature), not misreported. - Lists: `[3, 1, 4]`, or ranges `[1..20]`, `[0, 0.5..10]` (the first two elements set the step), `[10..1]` (counts down). Range bounds are numbers or constants defined in another row, like Σ bounds. List math broadcasts elementwise — `2L + 1`, `sin(L)`, `L^2`. A list is a variable that ranges over its values: the row is evaluated once per instance, so every use of the SAME list moves together (zips) — `s = [1..50]/5` then `(s, sin(s))` is a scatter of samples, and lists derived from `s` (`q = s^2`, `s[s > 2]`, `a_[s]`) stay paired with it. Lists with DIFFERENT origins are independent and cross: each `[…]` literal is its own origin, so `([0,1],[0,1],[0,1])` is the 8 corners of a cube, `[1,2] + [10,20,30]` has 6 elements, and two separately defined lists `(a, b)` make a grid. Columns of one data file share its rows, so `(data.x, data.y)` pairs up. To pair literal values write the points out: `P = [(1,4), (2,5), (3,6)]`; then `P.x` and `P.y` (and `P.z`) are the lists of their coordinates, paired the same way — `(P.x, 2 P.y)` is 3 points, not 9. Brackets add: a list inside a list contributes its elements, so with `n = [1,2]`, `[n, 3, 5]` is `[1, 2, 3, 5]` (a new list, independent of `n`). A bracket around one item is that item: `[n]` is `n` (so `n + [n]` has 2 elements, like `n + n`), and `[x + 1] 2` groups. `[]` is the empty list: combined with anything it gives `[]`, and `count([])` and `total([])` are 0. - Cases over a list decide each element: `{L > 2: L}` with `L = [1..4]` is `[3, 4]`, and an element no case holds for (with no otherwise) is not in the list at all, so `count({L > 2: L})` is 2. A whole point goes at once, and over lists a case may test equality. A function's point parameters read coordinates as `p.x`, `p.y`, `p.z`: `c(p, q) = {p.y = q.x: (p.x, q.y)}` composes arrows i → j and j → k, and with lists of arrows `A`, `B`, `AB = c(A, B)` is the arrows that meet (their matrix product, as a multiset of arrows), and `c(A, A)` pairs each arrow with itself: one name is one choice. Two independent draws from one list are binders, rows of their own: `p ∈ A` (or `p \in A`) names a choice from A, equal to A but never identical, so with `p ∈ A` and `q ∈ A`, `c(p, q)` is A composed with itself (`p + p` still zips; `p + q` crosses). A binder cannot use a name the language has: x, y, z, t, u, v, w, d, e, i, pi, a name starting u_, or a built-in function such as sin. A binder draws members, not a range: `a ∈ [0, 10]` is 0 or 10; every number from 0 to 10 is `a = interval(0, 10)`, and the whole numbers are `a ∈ [0..10]`. - List reductions: `mean(L)`, `total(L)`, `count(L)`, `min(L)`, `max(L)`, `stdev(L)` (sample), `median(L)`. A row that reduces to a constant shows the numeric value as a readout. mean/total/count/min/max expand symbolically, so elements may animate (`mean([sin(t), 2])`); stdev/median/sort need constant elements. A function applies to one member of a list argument at a time, a reduction in its body included: with `f(m) = total(L m)`, `f(M)` is one total per member of M (and `total(f(M))` their total), a number reduces as its one member (`mean(5)` is 5), so a wrapper `avg(s) = mean(s)` gives `avg(L)` = L back: reduce a list in the row, `mean(L)`. Over a list of points the body reads `p.x`, `p.y`. Over a column, a missing cell is left out of every reduction (as it is of `hist`) except `count(L)`, which answers how many rows there are. - `hist(L)` bins a list into a bar chart — `hist(L, 40)` sets the bin count (default ≈ √n, between 5 and 60). It is a whole row, not a value, and the one thing you may do to it is scale it: the plane has a single scale for both axes, so counts in the thousands need `hist(L, 40) / 800` to share a view with values in the units. Missing values are left out. - Order lives in tuples. A list `[ … ]` has no order — `[3, 1, 2]` and `[1, 2, 3]` are the same list — so anything that needs one takes a tuple `( … )`. `sort(L)` is L's values in ascending order, as a tuple, and `sort(P, key)` orders a list by a key written in that list: `sort(P, P.x)`, `sort((s, sin(s)), s)`. A tuple of 2 or 3 numbers is a point (`sort([3, 1, 2])` draws the point (1, 2, 3)); a longer one reads out, `sort([5, 3, 8, 1, 2])` = (1, 2, 3, 5, 8), and draws nothing. Tuples of one length combine position by position, like vectors: `sort(A) + sort(B)`. A tuple of points, `T = ((0, 0), (1, 1), (2, 0))`, is one value in order — `polyline(T)` walks it — and a square one is also a matrix. - Indexing is 1-based and needs a tuple: with `T = sort([7, 5, 6])` in a row above, `T[2]` is 6 and `T[k]` takes k from a slider. `T[2..3]` is (6, 7), and a list of indices picks one element each: `T[[3, 1]]` is `[7, 5]`, and `T[N]` for a named list `N` pairs with `N`. `L[2]` on a list `[ … ]` is an error that says to sort it first; a filter `L[L > 2]` is no index and works on any list. A named tuple or list indexes, and so does a sort call, `sort(L)[2]`; `x[2]` is plain multiplication (2x) for any other name. On a list of points or tuples, which has no k-th element, `[k]` is position k of each: with `A = [(1, 1), (1, 2)]`, `A[1]` is `A.x`, paired with `A.y`, and `Q[4]` is the 4th entry of every 4-tuple in Q. `.x`, `.y`, `.z` name the coordinates of a point (2 or 3 of them), on one named point too (`P.x`); a longer tuple is read by position only. A function's parameters index (`c(p) = p[4]`), and tuple arguments arrive whole when they fill the parameters: `c((3, 12, 6, 2))`, `c((1, 2), (3, 4))`. A recursive function's parameters are numbers: pass a point's coordinates. - Data columns: with `person = open("people.csv", 3a7f9c1d2e4b)` in a row above (see Definitions), `person.age` is a list of that column's numbers. Everything lists can do applies: `(person.age, person.height)` is a scatter, `mean(person.age)` a readout. File order is not part of the data: each record's position in the file is `person.row` (`row_index` when the file has its own `row` column), so `sort(person.age, person.row)` is the column in file order, and `polyline(sort((person.x, person.y), person.row))` draws a series in it (or sort by the x column itself). Naming works throughout — `half = person.age / 2`, `P = (person.age, person.height)` — and a named scatter is still a scatter. Only the device that dropped the file has its data — the link carries the file's name and hash, not its bytes. - A comparison over a list keeps the members it holds for: `L > 2` (or `L[L > 2]`) is the elements above 2, `count(L > 2)` how many. It keeps members of the list, not the values compared: `L^2 < 4` keeps elements of L, `P.x < 0` keeps points of P. Inside `{…}` a comparison is still one condition per element: `{L > 2: 1, 0}`. Index by a comparison to choose one column by another: `person.height[person.age >= 18]` (rows stay aligned), chains, `person.age[30 <= person.age < 40]`, and text, `person.height[person.city == "NYC"]` (`!=` too, and either quote). Filtering a whole file names a new one, `adults = person[person.age >= 18]`, whose columns work like any others (`(adults.age, adults.height)`). A filter settles when the row compiles, so it may use constants and sliders but not t; a comparison with a missing cell is false, so filtering also drops gaps. - Text columns are lists of text. They are not plottable and have no arithmetic directly. `person.name.length` returns a numeric list of character counts (Unicode code points) and plots one count per row. Missing cells stay gaps; counts use the parsed, trimmed text. Use it in arithmetic, reductions, histograms, and filters: `hist(person.name.length)` or `long_names = person[person.name.length > 10]`. `count(person.name)` still counts rows. `==` and `!=` exist only for filters: an equation still takes a single `=`. - Comments: a row starting with `#` (e.g. `# tangent lines`) plots nothing; in the app it renders as a heading whose group (the rows until the next `#` row) can be collapsed. Use them to label sections of longer documents. - Notes: `#` after a row's math starts a note to the end of the row (`y = x^2 # parabola`, `a = 2 # slope`). Only the math before it is read; the app shows the note in small text under the row. A `;` still ends the row, note included. - Row colors: a note that opens with a hex color draws the row in that color (`y = x^2 #e24`, `y = sin(x) #1f77b4 wave`). The 3 or 6 hex digits must touch the `#`; `# 100 samples` (with a space) stays a plain note. Without one, the rows that draw take the palette in order (blue, red, green, purple, orange, black); definitions, comments and view rows take no color. ## Variables (what a row means) - `x`, `y`: the 2D plane. `y = f(x)`, or any implicit equation in x and y (`x^2+y^2=4`), renders as a curve. A row is drawn per pixel when it depends on x or y, so a bare expression like `sin(x)` is a scalar field, not a curve: write `y = sin(x)` for the graph. - `z`: using z switches to 3D. `z = sin(x)cos(y)` is a surface; `x^2+y^2+z^2=9` is an implicit 3D surface. A bare expression in x, y, z is a field in space, drawn as a translucent cloud (see Scalar field); write `… = 0` for its surface. - `t`: time in seconds. Any row mentioning `t` animates, e.g. `y = sin(x - 2t)`. - `u`, `v`: the parametric parameters, two intervals (below) that are `u = interval(0, 1)` and `v = interval(0, 1)` unless a row redefines them as another: with `u = interval(0, 2pi)`, `(cos(u), sin(u))` is the whole circle. A row whose value is a point traces curves and surfaces, however it is written: `(u, u^2)`, `(0,0,1) u` and `u e_z` are all curves; a complex expression in u alone is a path (`exp(i 2 pi u)`). A bare real row in u and v alone draws the values it takes as a density curve: `u^2` has density 1/(2√x), `u v` has −ln(x). - `interval(a, b)`: every real number from a to b (a < b; the bounds may use sliders and t). `[1..2]` is only the whole numbers 1 and 2. Every mention of one name is the same number, so with `r = interval(1, 2)`, `r + r` is 2r; each written `interval(…)` is a separate one, so `interval(0, 1) + interval(0, 1)` is the sum of two. In a tuple it traces with u and v: `(r cos(2 pi u), r sin(2 pi u))` is a filled annulus, `(r, r^2)` a curve over [1, 2]. With a list it crosses into one curve per member, as u does: `(interval(0, 1), 0) + (0, [1, 2])` is two segments, at heights 1 and 2. A row in x and y over one interval is a family, drawn as the region its members sweep: with `a = interval(1, 2)`, `y = sin(a x)` shades every point some a from 1 to 2 puts on its curve, and `y < a x` every point below some member. At most one interval in such a row, and a bare field (`sin(a x)`) is an error: set it equal to something. Alone, an interval row draws its values against length: `interval(0, 10)` is the line at height 1 over [0, 10]. - Reductions over continuous sets are integrals: `count` is the measure, `total` the integral, `mean` their ratio. Over u, v and intervals the measure is length: `total(u^2)` is 1/3, `count(interval(1, 3))` is 2, `count((u, u^2))` is 1 (a parametric set carries its parameter's length). Over x (all of ℝ) `total(exp(-x^2))` is √π, `count(x)` is ∞, and a total that does not converge is an error. A filter is the set it keeps, measured in its own dimension: `count(x^2 + y^2 < 1)` is the area π, `count(x^2 + y^2 = 1)` the length 2π, `count(x^2 = 2)` the 2 roots; other reductions take its members, so `total(0 < x < 1)` is 1/2 (a region's members are points: use `count` for its area). Reduce a value over a set with `{conditions: value}`, the conditions joined by "and" (only here may one be an equation): `total({x^2 + y^2 < 1: x y})`, `mean({y = x^2, 0 < x < 1: y})`. The measure depends on the set, not the spelling (`2y = 2x^2` agrees with `y = x^2`), and never on the view: an unbounded region counts ∞, and a count of roots that cannot all be found (`sin(x) = 0`) is an error — restrict it, `count({-10 < x < 10, sin(x) = 0})`. `min`/`max` work over u, v and intervals and over points; `stdev`, `median` and `hist` of a continuous set, sets in space (x, y, z) and totals over unbounded plane regions are errors. A list in the argument keeps the list reduction: with `L = [1, 2, 3]`, `y = total(L x)` is the line y = 6x. - `w`: the complex plane point x + iy. A complex-valued expression in `w` (e.g. `ln(w-2) - ln(w+2)`) draws field lines and equipotentials. Wrapping it in `domain(…)` or `conformal(…)` picks a different view of the same function. - `z` inside `iter(…)`: the iterate being fed back through the step. Outside `iter`, `z` is the 3D axis as above. ## Row types - Curve: an equation in x and y — `y = x^2`, `(x^2+y^2)^2 = 8(x^2-y^2)`. - Region: inequalities, including chains — `y < x/2 + 1`, `4 <= x^2 + y^2 <= 9`. Non-strict comparisons draw a solid boundary. - Scalar field: a bare expression in x and/or y — `sin(x)cos(y)`, or `sin(x)` (constant along y) — shaded by value: the row color where it is positive, the color's complement where negative, stronger the larger it is. There is no implicit graph; a row in x alone reads out how to write its curve (`y = sin(x)`). A list of fields (`a = [1, 2]; sin(a x)`) is an error: write `y = sin(a x)` for the curves. - Scalar field in space: a bare expression in x, y and z — `x y z`, `z exp(-sqrt(x^2 + y^2 + z^2)/2)` — is a translucent cloud, tinted the same way (row color positive, complement negative) and denser where the field is larger. The density is relative: the scale is what |f| typically reaches in the view, so `x y z` and a tiny orbital both show, and a slider that only multiplies the field changes nothing. The cloud fills a soft ball around the view's center, fading out before the camera; a field that dies away (an orbital) shows whole, and zooming out shows more of one that does not (`x y z`). An implicit surface in the same graph is seen through the cloud in front of it, so `f(x, y, z) = …; f(x, y, z); f(x, y, z) = 0.5` draws a level surface inside its field. For nested level surfaces instead, write `f(x, y, z) = [1..5]`. - Value: a bare expression with no plot coordinate — `2+2`, `sqrt(a)` with a slider a, `|A - B|` between named points, `int[0..1] x^2 dx`, `sin(t)` — draws nothing (a lone definite integral shades its area) and reads out `= 4` (or `≈ 1.41421`) under the row, live as sliders, states and t move. The tool result reports it as kind `value` with the number in `value`. A number is not assumed to mean `y = number`: write `y = 4` for the horizontal line. - Complex point: a constant complex expression such as `1+2i` draws at (1, 2). Constants and t may animate its real and imaginary components. A named one, `a = e^(i pi/3)`, is written in wherever `a` is read (`a^2`, `domain(w - a)`, `[1..|a|]`), and a real value reached through complex ones is a constant like any other (`b = re(a)`, `r = |3 + 4i|`). - Complex list: one complex member makes a list complex — `1` in `[1, i, -1]` is 1 + 0i — and it draws each member as a complex point: `n = 5; e^(2 pi i [0..n-1]/n)` is the fifth roots of unity, n on a slider. Name it like any list (`S = e^(i pi [0..5]/5)`); `re(S)`, `abs(S)` and `(re(S), im(S))` are real. `total`, `mean` and `count` reduce it; `sort`, `median`, `stdev`, `min`, `max` and `hist` need real numbers, so take `abs`, `re` or `im` first (filters and sort keys too: `S[re(S) > 0]`; equality needs neither, `S[S == 1]`). `sort(S, re(S))` is S's members in order, a tuple, which has no picture yet: `T[k]` picks one. Members are drawn split into real and imaginary parts, 2^21 nodes in all: constant members are two numbers each, but thousands of members of a slider-dependent polynomial are too many. A list cannot mix complex numbers with points, and a point's coordinates stay real (`(S, 1)` is an error). - Complex roots: `w^3 = 1` draws three solution points; equations in w become two real constraints. This is a numerical search over the visible window, with exact labels for rational/quadratic polynomial factors and a defining- polynomial label for remaining algebraic roots. The optional certification control proves completeness only when every region in its search box is settled. - Complex path: a complex-valued expression whose only variable is u (constants, sliders and t aside) draws the path it traces over u, at (re, im) — the same plane as complex points and roots. `exp(i 2 pi u)` is the unit circle; with `f(w) = w^2 + w`, `f(exp(i 2 pi u))` is its image under f. Supported: arithmetic, powers, exp ln log sqrt, sin cos tan sinh cosh tanh, re im abs arg conj, user functions. Branch cuts (`sqrt`, `ln`, fractional powers) break the curve where the value jumps. An equation, inequality, or a use of v is an error, as is a path whose split into real and imaginary parts is too large (deeply nested products). An expression that mentions i but is real for every u (`abs(exp(i u))`) is not a path and says so. - Complex plot: a complex expression in `w` — `w + 4/w`. - RGB color field: `rgb(red, green, blue)` fills the 2D plane with a color at each (x, y). Channels are real expressions on the 0–1 scale (not CSS's 0–255); values outside that range are clamped, so `rgb(255, 128, 0)` is white, not orange. `rgb(1, 0, 0)` fills the plane red. Expressions may use x, y, t, sliders, coordinate fields, and real projections of w such as `arg(w)` or `abs(w)`. For example, `rgb(cos(arg(w)/2)^2, cos(arg(w)/2+1)^2, cos(arg(w)/2+2)^2)`. This is a whole-row plot mode, not a color value for styling another row; it cannot be nested in arithmetic or lists and does not take u, v, or z. A pixel with any undefined or infinite channel is transparent, so a partial piecewise channel can restrict the field: `rgb({x>0:1}, 0, 0)`. An optional fourth argument is the opacity, 0–1 (clamped; default 1), so a field can fade into the graph paper: `rgb(1, 0, 0, exp(-x^2-y^2))`. RGB, HSL and OKLCH rows share the same layer and draw in row order; curves and regions draw over them. - HSL color field: `hsl(hue, saturation, lightness)` uses hue in radians and saturation/lightness on a 0–1 scale (not CSS's percentages). Hue wraps around 2pi; saturation/lightness are clamped. `hsl(2pi/3, 1, 0.5)` is green. An animated wheel is `hsl(arg(w)+t/3, 1, 0.5)`. - OKLCH color field: `oklch(lightness, chroma, hue)` uses lightness 0–1, nonnegative chroma (typically 0–0.4), and hue in radians. Lightness is clamped, negative chroma becomes zero and hue wraps around 2pi. Zero chroma is neutral gray; lightness 0 and 1 give black and white. Colors outside sRGB are mapped by reducing chroma at fixed lightness and hue, then encoded as sRGB. This is strict chroma reduction, not CSS's local-MINDE algorithm or wide-gamut output. Try `oklch(0.72, 0.16, arg(w)+t/3)` for a wheel with more even perceived lightness than HSL. HSL and OKLCH have the same real-channel, whole-row and transparency rules as RGB; all three accept equations, sliders and time. Every channel is 0–1 except hue (radians, like trig functions and `arg`, so `arg(w)` is a hue directly) and chroma. To port a CSS color, divide 0–255 channels by 255, percentages by 100, and write a degree hue as `h pi/180`. - Domain coloring: `domain(f)` where f is complex — `domain(w^2 + 1)`. Hue is arg f, brightness |f|; zeros render black and poles white. - Conformal map: `conformal(f)` — `conformal(w^2/4)` — draws the image of the coordinate grid under f (the level curves of re f and im f). - Fractal: `iter(step)` iterates `z ↦ step`, coloring by escape time. Inside `iter` only, `z` is the iterate. If the step mentions the plane (`w`, `x`, `y`) iteration starts at 0, so `iter(z^2 + w)` is the Mandelbrot set; with a fixed constant the pixel is the starting point, so `iter(z^2 - 0.8 + 0.156i)` is a Julia set. An optional second argument sets the iteration count, `iter(z^2 + w, 600)` (default 250, max 5000). Any expression works: `iter((|re(z)| - i |im(z)|)^2 + w)` is the burning ship. `iter` is for the plane; an escape-time fractal in space (the Mandelbulb, a quaternion Julia set) is a recursive function — see "Recursive function" below. - Point: `(2, 3)`; animate with t: `(2cos(t), 2sin(t))`. - Data list: `[3, 1, 4, 4]` is a dot plot on the number line — a dot at each value on the x axis, copies stacked upward, so 4 has dots at (4, 1) and (4, 2). A list has no order, so no index is drawn (a sequence `a_n` does draw at n). Values closer than a dot's width at the current zoom share a column, so a long list (`sin(L)` for `L = [1..300]`, a CSV column of thousands) stacks into the shape of its distribution rather than one row of overlapping dots; zoom in and they separate. `[(1, 2), (3, 4)]` draws a scatter of points. Any list-valued expression plots the same way (`L^2 + 1`, `(L, L^2)` with `L` defined above, or a CSV column: `person.age`). - Geometry: with points defined in rows above (`A = (0, 0)`), `segment(A, B)`, `polyline(A, B, C, …)` (an open path through the points, not filled), `vector(A, B)` (an arrow from A to B; `vector(V)` draws V from the origin — the head keeps its size on screen), `line(A, B)` (the infinite line through A and B), `polygon(A, B, C, …)` (filled), `square(A, B)` (erected on side A→B, to the left of the direction A→B), and `circle(center, radius)` draw figures that follow their points. Point arithmetic works anywhere: `A + B`, `2A`, `(A + B)/2`, `midpoint(A, B)`, `perp(A)` (rotate 90° counterclockwise), `rotate(P, a)` (turn by a about the origin; `rotate(P, a, C)` about C; in 3D `rotate(P, a)` turns about z and `rotate(P, a, axis)` about axis), `unit(A)`, `dot(A, B)`, `cross(A, B)`, `|A - B|` (distance). Measurements: `distance(A, B)` is `|A - B|`; `angle(A, B, C)` is the angle at B, turning from B→A to B→C — signed, counterclockwise positive, in radians in (−π, π] (`angle(A, B, C) 180/pi` for degrees; wrap it in `abs(…)` for the unsigned angle) — and `angle(U, V)` is the same from vector U to vector V. Both are plain numbers: a bare row reads out "= value" (nothing is drawn — no arc marker), and they work inside any expression (`circle(A, distance(A, B))`). A scalar list inside a point broadcasts as in any scalar math (`distance(A, (L, 0))` is a list of distances); a list of points as an argument broadcasts too, and `T[1]` of a tuple of points selects one. An angle with a zero-length arm (`angle(A, A, C)`) reads "undefined". A parametric point like `A + u(B - A)` traces the segment. Tuple literals work directly as arguments (`segment((0, 0), A)`) and as operands: `A + (1, 2)`, `((0, 0) + B)/2`, `|(3, 4)|`. - Geometry in space: `A = (1, 2, 3)` defines a 3D point with scalar components `A_x`, `A_y`, `A_z`. `segment`, `polyline`, `polygon`, and `vector` accept matching 2D or 3D points, including tuple literals. 3D triangles fill; larger 3D polygons draw their outline. `cross` returns a vector in 3D. 3D `angle` is unsigned in [0, pi]; zero arms are undefined. `line(A, B)` of 3D points is the infinite line through them, drawn across the view box. `perp`, `square`, and `circle` remain planar; 3D dragging is not defined. Point lists draw as dots in space too: `[0,1] e_x + [0,1] e_y` is 4 points, `[e_x, e_z]` 2, and `([0,1],[0,1],[0,1])` the 8 corners of a cube. - Projective geometry (lines and planes as values): `join(A, B)` (or `line(A, B)`) is the line through two points, in the plane or in space; `join(A, B, C)` (or `plane(A, B, C)`) the plane through three points of space, and `plane(P, n)` the plane through P with normal n. `meet(L, M)` is where two lines of the plane cross, and in space where a line goes through a plane (a point) or two planes cross (a line); two lines of space do not meet. A meet is a point like any other — `X = meet(L, M)` names it, `polygon(meet(…), B, C)` and `midpoint(meet(…), A)` take it. Parallel lines meet at an ideal point, which draws nothing and reads out `at infinity, direction (1, 2)`. `project(P, L)` is the point of the line or plane L nearest P (or a line's shadow on a plane), and `reflect(P, L)` mirrors P in a line, a plane or a point. `distance(P, L)` measures from a point to a line or plane, and between lines or planes (0 where they cross); `angle(L, M)` is the acute angle between two lines or planes, in [0, π/2]. A row holding a line or plane draws it — a line of the plane as its implicit line, a line of space across the view box, a plane as a surface — and reads out its equation: `= y = 2x + 1`, `= z = -x - y + 1`, or `= through (0, 0, 1), direction (1, 1, 0)` for a line of space. Name one (`L = join(A, B)`) to pass it on; the definition itself draws nothing, the row `L` does. A line or plane is no number or point, so `2 L` is an error. Lists broadcast as everywhere else: with `P = [A, B]`, `join(P, C)` is two lines, `L = join(P, C); meet(L, L)` pairs each line with itself, and `meet(K, H)` for a line K and a list of planes H is the list of crossing points, which `count`, `mean` and `total` take. A plane draws as a translucent sheet, outlined where it meets the view box, so a line in it or behind it shows through. - Rigid motions (motors): `motor(L, th, d)` turns by th about the line L of space — right-handed about L's direction, A → B for `line(A, B)` — while sliding d along it (a screw; d may be left out); `motor(P, th)` turns the plane counterclockwise about the point P. `rotate(X, M)` moves a point, a list of points, a line, a plane or a figure by it: `rotate(hull(([0,1],[0,1],[0,1])), motor(line(A, B), t, t/4))` screws a cube along the axis through A and B, which stay draggable. `slerp(M1, M2, s)` is the rigid motion s of the way from M1 to M2, at constant speed along the screw between them — for motors that stay fixed (fixed axes, turns and slides), with the motion in s: `rotate(X, slerp(M1, M2, (1 - cos(t))/2))`. A row holding a motor draws its axis (or its centre, in the plane) and reads out what it does: `= turn 1.5708 about the line through (0, 0, 0), direction (0, 0, 1), slide 2`, `turn 1.5708 about (1, 1)` or `slide (0, 0, 3)` — a motor and its opposite are one motion, read the shorter way round. A motor is no number or point: name it (`M = motor(L, t, 1)`) and pass it to rotate or slerp. - Convex hulls: `hull(A, B, C, …)` or `hull(P)` for a whole point list draws the convex hull — a filled polygon in 2D, a lit opaque solid with flat-shaded faces and outlined edges in 3D (coplanar facets merge, so a cube shows six squares). Interior and repeated points are ignored, the points may move with sliders or t (the hull is rebuilt each frame), and flat 3D input degrades to a polygon. Up to 2000 points in 3D. Point figures transform as a whole — `R hull(P)`, `2 polygon(A, B, C) + (1, 0)`, `-segment(A, B)`, `rotate(hull(P), a)`, `e^(t cross(n)) hull(P)` — meaning the figure of the transformed points (the same as `hull(R P)`; works for segment, polyline, polygon, vector and hull). A LIST in the transform draws one figure per element: `th = 2pi [0..4]/5; rotate(hull(P), th)` is a rosette of 5 hulls, while `hull(rotate(P, th))` is the single hull of all 5 copies. Point lists also transform on their own: `R P`, `P + (1, 0)`, `rotate(P, a)`. It composes with crossing lists and rotations: `hull(([0,1],[0,1],[0,1]))` is a solid cube, `hull(([-1,1],[-2,2]))` a rectangle, and `phi = (1+sqrt(5))/2; k = 2pi [0..2]/3; hull(rotate((0, [-1,1], [-phi,phi]), k, (1,1,1)))` a solid icosahedron. - Object families: a list in a scalar position draws one object per element: `y = [1,2,3] x`, `circle((0,0), [1,2,3])`, `(cos(u),sin(u),[1,2,3])`. Uses of the same named list move together (`m = [1,2]`, `y = m x + m` is 2 lines); independent lists cross (`y = [1,2] x + [0,1]` is 4 lines, `circle(([0,1],[0,1]), 1)` is 4 circles). Limit 32 objects, in 2D or 3D, or 8 implicit surfaces, curves of intersection or fields in space (each is raymarched or traced on its own) — except point figures (segment, vector, polyline, polygon, hull), which are cheap and allow 1024: a deformation drawn as arrows is `a = [0..20]; b = [0..20]; P = (a, b); f(x,y) = (x + y/2, y + sin(x)/2); vector(P, f(P))`. Shader members share one program with per-draw uniforms. Scalar fields/RGB fields/domain coloring/fractals cannot superimpose as families; use a selected list element. Point-list arithmetic works: `Q = P + (1,0)`, `2P`. `polyline(T)` / `polygon(T)` walk a whole tuple of points in order, `T = ((0, 0), (2, 0), (1, 1))`, and a list only once it is sorted: `polyline(sort(P, P.x))`, a CSV scatter `polyline(sort((data.x, data.y), data.x))`; a list `[ … ]` has no order to walk. `hull(P)` takes any list. Range bounds may use sliders, but not t; families re-expand on bound changes. - Sequence values: `a_n = 1/n` defines `a_3` as a scalar and `a_[1..10]` as a list, usable in sums, scatters, histograms and regression. `a_k` reads the term at a slider `k`, and `a_N` one term per element of a list `N`. A term may use another sequence at its own index — `a_n = b_n + 1`, `b_[n+1] - b_n`, `sum(k=1..n, b_k)` — including a recurrence's terms (`d_n = a_{n+1} - a_n`, up to index 1000). A recurrence that steps from its own term alone draws a cobweb (or, with x, a bifurcation diagram). One whose step also reads n, another sequence at n, or a tuple at a position in n draws its terms as dots instead: `a_{n+1} = (n + 1) a_n` (factorials), `q_{n+1} = step(q_n, D[n + 1])` with `D = (1, 1, 0, 1)` (a state machine's run over the digits of D). Its terms stop where the tuple does. Two recurrences may read each other at n (`q_{n+1} = q_n + r_n` beside `r_{n+1} = r_n + q_n`), and a case in a sequence row may test equality (`{mod(n, 2) = 0: 1, 0}`). A sequence's index may also go in braces or parens, as its recurrence row is written: `b_{n+1}`, `a_(n-1)`, and the rows `a_{n} = 1/n` and `a_{0} = 1`. Each letter is one sequence: beside `a_n = 1/n`, `a_k = 7` is a constant named a_k. Indices must be whole numbers from 0 to 1000. Recurrences use their `a_0` seed (default 0.5); terms compute as a bounded chain of scalar constants, so sliders and t remain live. A recurrence using x as a bifurcation parameter cannot also supply constant terms without binding that parameter. - Tuple-valued recurrences: a recurrence whose step calls `push(s, a)` (s with a appended) or `pop(s)` (s without its last element), or whose value is a tuple, has tuples as terms — a stack. `top(s)` reads the last element. `s_0 = ()` starts it from the empty tuple (the default), and any tuple may be its seed; a tuple seed with a step that makes no tuple (`a_0 = (0.1, 0.2); a_{n+1} = 3 a_n (1 - a_n)`) steps each element instead. `count(s_n)` is its length and `s_n[h]` its h-th element; a number with a tuple applies to each element (`1 + p_n`, `s_n/2`). Only the case that holds is run, so cases may differ in length: with `D = (1, 1, 2, 2)`, `s_{n+1} = {D[n + 1] = 1: push(s_n, 1), pop(s_n)}`. The run stops where it pops or reads the top of the empty tuple, or where no case holds (a machine with no move). The row draws its terms as rows of a lattice: row n, position h. A pushdown automaton is a state recurrence and a stack recurrence reading each other: `q_{n+1} = step(q_n, D[n + 1], top(s_n))`. One that starts from a point (`p_0 = (0, 0)`, `p_{n+1} = (1 - 1.4 p_n[1]^2 + p_n[2], 0.3 p_n[1])`, the Hénon map) and stays one draws its orbit as points. Other rows read its terms at a number or a slider (`s_3`, `top(s_k)`); an explicit sequence `d_n = count(s_n)` cannot (read it in a recurrence's step instead). Its terms are computed once, so its step cannot read t. - Cellular automaton (1D): `c_{n+1}[i] = …` is a rule for a row of cells, reading the previous row at fixed offsets — `c_n[i-1]`, `c_n[i]`, `c_n[i+2]` — plus n, t and constants (not i itself). `c_0[i] = …` is the starting row as any expression in i; without it the start is a single 1 at i = 0 on a background of 0s. The rule row draws the space-time diagram in a lattice panel (below): i across, n down, for 1000 steps. The lattice is unbounded: each side's background steps by the rule too, so rule 1 (000 → 1) flips the whole background every row. Values shade the row's colour from 0 (empty) to the largest value (solid), so k-state rules work too. Every elementary rule on one slider: `r = 30; c_{n+1}[i] = mod(floor(r / 2^(4 c_n[i-1] + 2 c_n[i] + c_n[i+1])), 2)` (frame it with `view(i = -60..60, n = 0..80)`). In automaton and table rows a piecewise case may test equality — `{s = 3: 1, 0}` — since cells are whole numbers; so may a function those rows call. - Cellular automaton (2D): `L_{n+1}[i, j] = …` reads the previous generation at fixed offsets, row first as in a matrix (`L_n[i+1, j]` is the cell below). A Σ with number bounds unrolls, so a neighbourhood is written once. The seed is `L_0[i, j] = …`, or the live cells as (row, column) points, `L_0 = [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]` (a glider). The rule row draws one generation, 8 per second as t runs, on a board of cells -256..256 each way (a pattern reaching its edge is cut off there), with the generation read out in the corner. Conway's Life: `life(c, s) = {s = 3: 1, s = 4: c, 0}` then `L_{n+1}[i, j] = life(L_n[i, j], sum(a=-1..1, sum(b=-1..1, L_n[i+a, j+b])))` (s counts the cell itself). - Table: `T[i, j] = …` is a function on the integer lattice, drawn as cells shaded by value: row i, column j, as a Cayley table or a matrix is read. Undefined values leave a cell empty, so nested cases bound it: `n = 6; T[i, j] = {0 <= i < n: {0 <= j < n: mod(i + j, n)}}` is the addition table of ℤ/n. Only the cells in view are computed, so a table may be unbounded (`T[i, j] = gcd(i, j)`). - Graph: `graph(from, to)` or `graph(from, to, label)` draws arrows between numbered vertices, laid out automatically. Its arguments are read as one tuple whose elements are the arrows, so lists combine by the usual rule: with `k = [0..11]; g = [1, 4]`, `graph(k, mod(k + g, 12), g)` is the Cayley graph of ℤ/12 by 1 and 4 (both k's chosen together, g crossing them), and with `Q = [0..2]; S = [0, 1]` and a transition function `step(q, s)`, `graph(Q, step(Q, S), S)` is every transition of a state machine. `graph(P)` takes a list of pairs, `graph([(1, 2), (2, 3)])`. An arrow listed twice is drawn once with its count (×2); an undefined vertex drops its arrow, so a case keeps the ones wanted — composing two multisets of arrows (matrix multiplication, as paths): `graph({A.y = B.x: A.x}, B.y)`, or with the composition named as above, `graph(c(A, B))`. Cases in graph rows may test equality. A panel of graphs has no grid. A piecewise works inline over a list or through a function alike: `k = [1..40]; graph(k, {mod(k, 2) = 0: k/2, 3k + 1})`. - `mark(v)` highlights vertex v in the graphs of its panel and reads v out; with a slider or t it walks, e.g. a state machine's run as a recurrence over a tuple of input symbols: `D = (1, 1, 0, 1); q_0 = 0; q_{n+1} = step(q_n, D[n + 1]); mark(q_N)`. - Lattice panels: a panel whose rows are all automata, tables and stacks (or whose view row names index axes) is a lattice. Its grid runs between cells, its edges are numbered by index (whole numbers only), and zoomed in far enough each cell prints its value. `view(i = -60..60, n = 0..80)` frames one by its index names, in either order; panning rewrites it in those names. - Decided comparisons: `2+2=4` reads “Always true (4 = 4)”; `e=2` reads “Never true (2.71828 ≠ 2)”. Comparisons involving sliders/time say “True now” or “False now”. They draw no degenerate implicit curve. - Identities: an equation in x, y, z and t whose two sides agree everywhere reads “Holds everywhere (checked numerically)” instead of drawing a curve. The check compares both sides at many random points, for any slider values, so it is evidence, not a proof. `sin(x)^2 + cos(x)^2 = 1` holds; `sin(x)^2 + cos(x)^2 = 1.0001` does not, and stays an (empty) implicit curve. The wave equation for d'Alembert's solution: `c = 1; g(s) = exp(-s^2); f(x, t) = (g(x - c t) + g(x + c t))/2; y = f(x, t); d^2/dt^2 f(x, t) = c^2 ∇^2 f(x, t)`. The pulse splits in two and the last row holds everywhere. (u and w are the parametric and complex variables, so they cannot name the solution.) - Vector field: a 2-tuple in x, y — `(-y, x)` — drawn as animated streamlines (line-integral convolution); clicking the canvas traces integral curves. A 3-tuple in x/y/z, or `(x',y',z') = (10(y-x), x(28-z)-y, x y-8z/3)`, draws 32 deterministic auto-seeded trajectories in space. Traces run in a worker with bounded RK4 steps and a directional fade. Moving fields are retraced at most four times per second. The arrows control switches to a 5×5×5 lattice of direction glyphs; there is no ambiguous 3D click-to-seed. - ODE / slope field: `dy/dx = x y` or `y' = sin(x) - y` plots the direction field (1, f); a system `(x', y') = (y, -sin(x))` plots its phase portrait. (Primes on x/y/z mean this. A prime on a name of your own is a state — see below — so `y' = -y` is a slope field while `th' = -th` is a simulation.) A system that conserves an energy H(x, y) flows along its level sets, so a row `H = c` draws an orbit: beside the pendulum `(x', y') = (y, -sin(x))`, `y^2/2 - cos(x) = 1` is its separatrix, the level through the saddles at ±π, with swinging inside and spinning outside. - Random variable: `X ~ Normal(mean, sd)` declares X and plots its density curve. Parameters may use sliders and t, so `X ~ Normal(0, a)` follows the slider live. - Probability: `P(X < 2)`, `P(X > 0)` or `P(0 < X < 2)` shades that area under X's density and shows the numeric probability (normal CDF) as a readout under the row. - Expectation: `E(X)`, `E(X^2 + Y)` — the mean of any expression in random variables, shown as a readout under the row and marked on the density. - Parametric curve: components in u — `(2cos(2pi u), sin(4pi u))`, or with `u = interval(0, 2pi)` above, `(2cos(u), sin(2u))`; three components make a 3D curve — `(2cos(6pi u), 2sin(6pi u), 4u - 2)`. A plane curve lifts the pen where it jumps (a step, a pole, a branch cut) rather than bridging the gap: `(u, floor(3u))`, `(4u - 2, tan(12u - 6))`. - Tube: `tube((x(u), y(u), z(u)))` sweeps a 3D parametric curve into a lit solid tube — `tube((sin(2pi u) + 2sin(4pi u), cos(2pi u) - 2cos(4pi u), -sin(6pi u)))` is a trefoil knot. An optional last argument sets the radius (default 0.1): `tube((1+cos(4pi u), sin(4pi u), 2sin(2pi u)), 0.06)`. It must be the whole row; without it a 3D curve stays a thin line. The curve needs exactly three components: `tube((a, b), r)` does not tube a plane curve, it reads as the 3D curve (a, b, r). - Parametric surface: three components in u and v — `(cos(2pi u)(2+cos(2pi v)), sin(2pi u)(2+cos(2pi v)), sin(2pi v))`. - Parametric region: two components in two parameters fill the region they trace — `(u cos(2 pi v), u sin(2 pi v))` is the unit disc, and with `r = interval(1, 2)`, `(r cos(2 pi u), r sin(2 pi u))` an annulus. In a 3D scene it lies in the plane z = 0. - Parametric solid: three components in three parameters (intervals, u and v) fill the solid they trace — `(interval(0, 1), interval(0, 1), interval(0, 1))` is the unit cube, and with `r = interval(0, 1)`, `p = interval(0, pi)` and `q = interval(0, 2pi)`, `(r sin(p) cos(q), r sin(p) sin(q), r cos(p))` the unit ball. It is drawn opaque, as the faces of its parameter box. A map that folds over itself inside the box (`s = interval(-1, 1)`; `(s^2, u, v)`) is an error: write it over a range where it does not, like `interval(0, 1)`. So is one undefined in part of the box (a `{…}` with no default, `sqrt(s)` over negative s): give the `{…}` a default, or split it into solids over the parts where it is defined. - 3D surface: any equation in x, y, z. - Surface of revolution: `revolve(f)` turns the curve y = f(x) about the x-axis — `revolve(sqrt(x))`, `revolve(sin(x) + 2)`, or `revolve(f)` with `f(x) = …` defined above. A second argument names another axis: `revolve(y^2, y)` revolves a profile in y about the y-axis (`z` likewise). The profile may use only its axis variable, plus sliders and t. It is exactly the implicit surface `y^2 + z^2 = f(x)^2`, so a negative f draws the same surface as |f|, and a piecewise f with no default bounds the solid: `revolve({0 < x < 3: sqrt(x)})`. It must be the whole row. - Random variable: `X ~ Normal(mean, sd)` — also `Uniform(lo, hi)` and `Exponential(rate)`, with short aliases `N`, `U`, `Exp`; a bare name means the standard parameters (`X ~ N` is Normal(0, 1)). The row plots the exact density curve; parameters may use sliders and t. - More continuous families, each with an exact density, exact `P(…)` and exact moments. Parameter order and meaning are fixed as follows: `Gamma(shape, rate)` — RATE, not scale: mean = shape/rate, and `Exponential(r)` is `Gamma(1, r)`; `Beta(a, b)` on [0, 1]; `ChiSquared(df)` (aliases `ChiSq`, `Chi2`) = `Gamma(df/2, 1/2)`; `StudentT(df)` (alias `T`, so `X ~ T(5)`); `LogNormal(mu, sigma)` where ln X ~ Normal(mu, sigma); `Cauchy(location, scale)`; `Weibull(shape, scale)`. Only `LogNormal` and `Cauchy` have a bare standard form, (0, 1). Names are case-insensitive and live only right of the `~`, so a constant `T` or a function `gamma(x)` does not collide with them. Shape, rate, scale, sigma, df, a and b must be > 0: a written-out bad value, or a constant holding one, is an error on the row naming the parameter (`Gamma(shape, rate) needs shape > 0.`). Densities with a pole at the support edge (`Gamma` with shape < 1, `ChiSquared(1)`, `Beta` with a or b < 1, `Weibull` with shape < 1) draw and shade up to the pole. - Discrete random variable: `X ~ Binomial(n, p)`, `Poisson(mean)`, `Geometric(p)`, `NegativeBinomial(r, p)`, `Bernoulli(p)`, `DiscreteUniform(a, b)` (aliases `Binom`, `Pois`, `Geom`, `NegBin`; case-insensitive, no bare standard form). The row draws the pmf as stems — a dot at (k, P(X = k)) on a thin line from the axis — at the whole numbers in view. Conventions, fixed: `Geometric(p)` is the TRIAL of the first success, support 1, 2, 3, … (mean 1/p); `NegativeBinomial(r, p)` counts FAILURES before the r-th success, support 0, 1, 2, … (mean r(1 − p)/p), and r > 0 need not be whole — so `NegativeBinomial(1, p)` is `Geometric(p)` shifted down by 1; `DiscreteUniform(a, b)` includes both whole-number ends, a ≤ b; `Bernoulli(p)` is `Binomial(1, p)`. Valid parameters: `n` a whole number ≥ 0; 0 ≤ p ≤ 1 for Binomial/Bernoulli (p = 0, p = 1 and n = 0 are a single stem of height 1, not errors); 0 < p ≤ 1 for Geometric and NegativeBinomial; `mean` > 0 (`Poisson(0)` is an error). A bad value — written out or held by a slider, e.g. n = 2.5 — is an error on the row naming the parameter (`Binomial(n, p) needs a whole number n ≥ 0 (n = 2.5).`); use `round(n)` for a slider with fractional steps. A parameter animated with t is not judged: at instants where it is invalid nothing is drawn. `P(…)` over one discrete variable with constant bounds is exact (closed-form cdf, any size of n or mean) and highlights the selected stems, and strict and non-strict bounds are different events: `P(X < 3)` stops at 2, `P(X <= 3)` includes 3, `P(2 < X <= 5)` is 3, 4, 5, `P(X < 2.5)` is `P(X <= 2)`. A bound within rounding of a whole number is that number (`a = 0.1*3*10` is 3), as for the parameters. Point events exist only here: `P(X = 3)` (also `==`) is that stem's height and `P(X != 3)` its complement; over a continuous variable they are an error (the value would be 0). `E(X)` is the exact closed-form mean, marked on the axis. Zoomed out past 1024 whole numbers in view (`Poisson(1000000)`, `DiscreteUniform(-1000000, 1000000)`) the stems merge into the filled outline of the same exact heights. - Arithmetic over discrete variables: `S = X + Y`, `D^2`, `D / 2`, `1 / N`, `{X > Y: X, Y}`, a bare `X + Y` row, `E(X Y)`, `P(X + Y = 7)`, `P(X > Y)`, `P(X = Y)`. A variable built ONLY on discrete bases is discrete: its row draws a pmf as stems — never a smoothed curve — at its atoms' true locations, which need not be whole numbers (`D / 2` stands on the halves, `D^2` on the squares, `sin(D)` wherever it lands); values equal up to rounding are one atom (`0.1 X + 0.2 Y` at X = Y = 1 is the atom 0.3). It is EXACT: the joint of the independent bases is enumerated (each unbounded base cut where its tails hold < 1e-12; independent parts convolve, so ten dice are cheap), the readout is `μ = …, σ = …`, and `E(…)` prints 4 places. The joint is over the BASES, so dependence is kept: `X + X` is 2X (even numbers only), `X - X` is the atom 0, `P(X > X)` is 0, and with `A = X + Y; B = X - Y`, `P(A > B)` is `P(Y > 0)`. Every `P(…)` keeps its strictness, joint events included — ties have mass: `P(X > Y)` + `P(X = Y)` = `P(X >= Y)` — and a bound is matched to an atom up to rounding (`P(S <= 0.1 + 0.2)` includes the atom 0.3). Where g is undefined or infinite (`1 / N` at N = 0) that mass is no value: the readout adds `P(defined) ≈ …`, as for densities. A mean whose sum diverges (`2^G` over `Geometric(0.3)`) reads `no stable mean (heavy tails)` / median and IQR instead of a truncated sum. Closed families answer through the law itself, at any size: a plain sum of independent Poissons is Poisson, of Binomials/Bernoullis sharing one p is Binomial, of NegativeBinomials sharing one p is NegativeBinomial (`X + X`, `2X`, `X + 1` are not — they enumerate). When the joint is too large to enumerate (more than 131072 points in one step: `A B C` over three `Poisson(30)`, anything over `Geometric(0.00001)`) the row falls back to the joint sample, says so — `μ ≈ …, σ ≈ … (sampled)`, `P(…)` to 3 places; a sampled moment whose tail cannot be certified is not printed (`σ unstable`, `σ = ∞`, or median and IQR with `no stable mean`) — and still draws a pmf: the frequency of each sampled value as stems, or past 1024 distinct values a histogram on the values' own lattice with heavy atoms (the 0 of `L B`, B Bernoulli) kept as stems; values on no lattice read `(sampled: too many distinct values to resolve the pmf)`. - Discrete mixed with continuous (`N ~ Poisson(3); Z ~ Normal(0, 1); Y = N + Z`, `N Z`, `P(N > Z)`, `E(N + Z)`) is a continuous MIXTURE: a density row, estimated from the joint sample like any other sampled density (an atom such as `N Z = 0` stays a stem). A point event needs discrete variables throughout: `P(Y = 3)`, `P(N = Z)`, `P(Z = 1)` are errors naming the continuous variable (the value would be 0). - Derived random variable: `Y = X^2`, `S = X1 + X2` — arithmetic over declared random variables declares a new one, including piecewise conditionals: `Y = {X > 0: X^2, 1}`. Distinct names are independent, so `S = X1 + X2` is the convolution; the same name stays dependent, so `X + X` is exactly 2X. A bare expression in random variables (`X + Y` as its own row) plots that density without naming it. Affine forms stay exact: combinations of normals (`Z = (X + Y)/2`) are the exact normal, a scaled/shifted uniform is a uniform, and sums of uniforms (`S = X1 + X2`, the central-limit demo) draw their exact piecewise-polynomial convolution — the triangle's apex is a true corner. Independent Gamma-family terms (Gamma, ChiSquared, Exponential) with positive number coefficients whose scaled rates agree sum to the exact Gamma (`X1 + X2` of two `Gamma(·, 3)`, `2X`, a sum of chi-squareds), and the square of a standard normal (`Z ~ Normal(0, 1); Z^2`) is the exact `ChiSquared(1)`. Any affine combination of independent Cauchys with nonzero number coefficients is the exact Cauchy (locations add, scales add through |c|: `X + 1`, `-3X`, `X + C`; a slider coefficient estimates instead, since at 0 the variable is a constant), and a LogNormal times a positive number is the LogNormal with mu + ln c. These rows report exact μ and σ where those exist. Everything else (products, nonlinear or piecewise transforms, mixed families) is estimated from 131072 joint samples and reports μ, σ as `≈`. A variable with heavy tails — anything that passes the tail of a `Cauchy` or a `StudentT` with df ≤ 2 through, like `X + 1` — reports `median ≈ …, IQR ≈ …` instead: its σ (and for df ≤ 1 its mean) does not exist, and a sample estimate of it would be noise. Whether the mean exists is read off the tails (it does when they decay faster than 1/x, with a two-standard-error margin — and a wider one over a `Cauchy` or `StudentT(df ≤ 1)` base — so a mean that only just exists reads as unstable), and a mean that numeric integration certifies always counts, so `E(X^2)` over `StudentT(3)` is 3 although its σ is infinite, while `E(X^2 + Y)` over `StudentT(2)` has no value. Point masses are detected and drawn as probability stems (height = mass): `Y = {X > 0: 1, 2}` is two stems, `floor(4X)` four, and a mixed result like `{X > 0: X^2, 1}` draws its stem plus the continuous density. - Probability: `P(X < 2)`, `P(-1 < X < 2)`, and bounds around one expression like `P(0.5 < X + Y < 1.5)` — exact (closed-form CDF or polynomial integration) with the area under the density shaded, whenever the bounded variable or expression has an exact law. Any other inequality — `P(Y > 0.5)` for a nonlinear Y, `P(Y > X)` — is estimated from the joint samples; the row shows the value as `≈`. - Expectation: `E(X)`, `E(2X + 3)`, `E(X^2 + Y)` — the mean of any expression in the declared variables, exact whenever the expression has an exact law (closed-form pdfs, affine combinations of normals, uniform sums) and the finite-sample mean otherwise. The row shows the value as a readout and draws a vertical marker at x = E under the expression's density. Where the mean does not exist (`E(X)` for a Cauchy or `StudentT(1)`, `E(X^2)` for a Cauchy) the readout is `no stable mean (heavy tails)` and no marker is drawn. ## Definitions (extra rows that set up the others) - Reserved names — never define these, use them as a constant, or put a slider on them: `t` is animation time in seconds (it plays on its own; any row using t animates), `x`, `y`, `z` are the plot axes, `w` is the complex variable x + iy, `i` is the imaginary unit, `e` is Euler's number, `pi` is π, and `d` is the derivative operator. `u` and `v` are the curve/surface parameters: they can't be numbers either, but they can be redefined as another interval (`u = interval(0, 2pi)`; the default is `interval(0, 1)`). None of these can name a function either: a reserved name followed by `(…)` multiplies, so `x(y - 1) = 1` is x·(y − 1) = 1, and `u(x, y) = x^2 + y^2` is u times a point ("u(x, y) reads as u times (x, y)"). Name your own functions f, g, h or a word, write `f(x, y) = …` and call f. Pick any other name for your own numbers: - Don't: `t = 3` then `(cos(2pi u), sin(2pi t u))` — t is time, so `t = 3` is rejected ("t is reserved for time") and the curve would keep changing as t counts up. - Do: `k = 3` then `(cos(2pi u), sin(2pi k u))` — a Lissajous figure with k lobes, and k becomes a draggable slider. (Good names: k, n, m, a, b, c, s, r, or a word like `twist`.) Only when you really mean to replace a built-in across the whole graph, define it with `:=` — `e := 0.6`, `pi := 3`, `i := 3`, `w := x - y`, or `t := 0.5`, which stops all animation — and the built-in meaning is gone from every row (`i := 3` makes `(1 + 2i)` the number 7). Only as a value: `e(x) := …` is rejected. `x`, `y`, `z` and `d` can't be redefined. - Constant: `a = 2` — the UI shows a draggable slider for it. Computed constants allowed: `b = a^2 + t`. Every definition may be written with `:=` instead (`a := 2`, `f(x) := x^2`): the same row, said explicitly, and the only way to redefine e, pi, i, w or t (see Reserved names). - Slider range and step: `a = clamp(0.4, 0, 1)` is a slider over [0, 1], and `n = round(5)` (or floor/ceil) steps by whole numbers; they combine, `r = round(clamp(30, 0, 255))`. The value is what the expression says — `clamp(5, 0, 1)` is 1 — and the range is part of the row, so it travels in the link. The ends may use other constants (`clamp(a, 0, b)`), not t. Dragging rewrites only the number; editing the slider's end boxes writes the clamp. `clamp(x, lo, hi)` is also an ordinary function anywhere. - Function: `f(x) = x^3 - a x` — call it from other rows; calls inline symbolically, so `d/dx f(x)` works. A function of several variables also takes ONE point in their place: with `f(x,y) = (x + y/2, y)` and `A = (1, 2)`, `f(A)` is `f(A_x, A_y)`, and so is any computed point — `f(R A)`, `f(A + B)`, `f(rotate(A, 1))`, `f(f(A))`. A list of points maps: `P = (a, b); f(P)` is the image of the whole lattice (one point per point of P), so `vector(P, f(P))`, `hull(f(Q))`, `f(R P)` and `f(P + (1, 0))` all work; a scalar `g(x,y) = x y` gives `g(A)` a number (`y = g(A) x`) and `g(P)` a value list. The point must have exactly as many components as the function has parameters (3D: `F(x,y,z)` takes `F(B)`). A matrix argument is read as the list of its rows (a tuple of 2 points in 2D is a 2×2 matrix, and `g(S)` maps it like any other point list). A point does not spread among other arguments, and built-ins (`atan2(A)`) do not take one. - Recursive function: a function may call itself when every self-call is a whole case of its `{…}` (tail position), so the call runs as a loop — per pixel on the GPU, like `iter`. `f(n, a) = {n <= 0: a, f(n - 1, a + n)}` sums 1..n; `f(z) = {re(z) >= 1: 1, f(4 - 3(z^6)^(1/6))}` with `f(x i - |y|) >= 0` shades the Koch snowflake. At most 250 passes run — more, up to 5000, when a parameter every self-call advances by one is capped by a case with a plain number (`k >= 600: …` with `f(…, k + 1)`); past that, or once a value leaves the finite range, the result is undefined and nothing is drawn — a region shades exactly where the recursion terminates. `n f(n - 1)`, a call inside a condition, and two functions defined through each other are errors (they would need a stack), as is `d/dx` of a recursive function. Parameters are numbers, but a self-call's point (a tuple, a named point, or a helper's result) spreads into consecutive parameters, and a quaternion into four: `G(T(a, b, c) + (x, y, z), k + 1)` passes `G(a, b, c, k)` four values. A recursive function of x, y, z set equal to a constant is an implicit surface, raymarched per pixel. For an escape-time fractal in space, return the orbit's Green function ln|p|/nᵏ (degree n, k passes) rather than the escape count — the count jumps, which the raymarcher cannot draw — and draw a small level of it. The Mandelbulb, whose "triplex" power raises r to n and multiplies both spherical angles by n: `n = 8; S(R, t, p) = R (sin(t) cos(p), sin(t) sin(p), cos(t)); r(a, b, c) = sqrt(a^2 + b^2 + c^2); T(a, b, c) = S(r(a, b, c)^n, n acos(c/r(a, b, c)), n atan2(b, a)); G(a, b, c, k) = {r(a, b, c) > 2: ln(r(a, b, c))/n^k, k >= 12: ln(r(a, b, c))/n^k, G(T(a, b, c) + (x, y, z), k + 1)}; G(x, y, z, 0) = 0.001`, framed with `camera(-pi/3, 0.4, 5)`; `+ C` for a fixed point C in place of `+ (x, y, z)` draws its Julia bulb. Thin detail finer than the raymarch step reads as dust at the edges. - Point: `A = (0, 0)` — a named, labeled point drawn on the canvas; the user can drag it and every row using it follows. Points may be computed from other points defined above: `C = B + D`, `M = midpoint(A, B)`, `P = midpoint(A, B) - perp(B - A)/2`. Scalars over points work too: `n = cross(B, D)`, `L = |B - A|`. A named vector that uses x, y, or z is a vector field, not a pinned point: `s = (x, y)` is the position vector and draws nothing of its own; later rows treat it as a point, so `dot(s, s) = 1` and `|s| = 1` are the unit circle, `|s| < 1` the unit disk. `F = (y, -x)` is any field of the plane. In space, `s = (x, y, z); dot(s, s) = 1` is the unit sphere. - Named curve or surface: a named vector that uses u or v is inlined the same way — `c = (cos(2pi u), sin(2pi u))` draws nothing of its own, and later rows use it as the point it traces: `c` draws the unit circle, `M c` its image under a matrix M, `2c + (1, 0)` a moved copy. A scalar works too (`k = u^2; (u, k)`) and goes wherever u itself could — so `y = k` is the same error as `y = u` — and so does a surface over u and v. A definition may use u, v or x, y, z, not both: `(x, u)` is an error. - Dragging: a point (named or bare) is draggable only where the drag has somewhere to write the new value: a coordinate that is a plain number (`(2, 3)`) or a bare slider name (`(a, 3)` with `a = 1` above) moves; computed coordinates (`C = B + D`, `(a+1, cos(t))`) are pinned, and 3D graphs have no point dragging. The MCP encode_graph_url result marks each point row with `"draggable"` — write coordinates as literals or sliders when the user is meant to move the point. - State (simulation): `th' = -sin(th)` defines the time-derivative of `th`, integrated forward (RK4) as the graph animates, starting from `th(0) = 2` (0 if omitted). Everywhere else `th` then behaves exactly like a constant, so plot the system by drawing with it: `bob = (sin(th), -cos(th))` names the swinging bob and `segment((0, 0), bob)` draws its rod — points, segment() and polygon() all accept state-valued coordinates. Use this for anything with no closed form — coupled oscillators, a double pendulum, Lorenz. - Vector state: a state whose derivative (or starting value) is a 2- or 3-vector integrates componentwise: `r' = vel; vel' = -r/|r|^3; r(0) = (1, 0); vel(0) = (0, 1)` is an orbit. Components are `r_1`, `r_2` (and `r_3`) — use them anywhere a scalar goes — and the bare name `r` draws as a moving point and works in point arithmetic (`perp(r)`, `dot(r, r)`, `r/|r|`, segment endpoints). 3-component states (Lorenz) plot through their components. - State family: a LIST of starting values runs the system once per element (up to 1024 runs): `p(0) = ([0..99]/10, 0, 0)` is 100 runs, crossing and zipping like any list. States coupled to it (`r' = vel` with a list of r(0)) run per element too, from their own single starting value. `p` then draws one moving point per run, `p_1` the list of first components (`mean(p_1)`), and figures built from the state (`segment((0, 0), b1)`) draw one per run. Started from a tuple, `p(0) = (sort([0..99])/10, 0, 0)`, the runs are in order and `p[1]` is the first run. - Orbit: `p(50..400)` draws the path the state takes from t = 50 to t = 400, integrated ahead of time from p(0) with the live simulation's own steps — the moving point rides exactly along it. Start after 0 to skip the transient onto an attractor. A family draws one path per run (keep it to a few, or pick one of runs in order: `p[1](50..400)`); a scalar state plots against time, `th(0..20)` being the curve (t, th). The range must be constant (sliders are fine; the orbit re-integrates when they move), 0 <= start < end; anything drawn from states works too, like `b2(0..30)` for a pendulum bob. Orbit plus family is how to draw a strange attractor with particles on it. - Data file: `person = open("people.csv", 3a7f9c1d2e4b)` names a CSV dropped onto the app. Drag a .csv/.tsv anywhere in the window and this row (plus a scatter of its first two numeric columns) is written for you; the file is parsed once and kept in the browser's local storage, pinned by the SHA-256 prefix in the row. Each numeric column reads as a list under its dotted name (`person.age`); text columns read as lists of text, which filters can compare; missing cells become gaps the row's readout counts. The delimiter is sniffed (`,`, `;`, or a tab), a title line above the header is skipped, and comma-form numbers in a `;` or tab-separated column select decimal-comma parsing (`1,5` is 1.5). Columns without comma-form numbers keep dot decimals (`1.234` is 1.234). The readout reports decimal-comma interpretation. Because the bytes never enter the URL, opening the link on another device (or validating it server-side, as the MCP does) reports the data as device-local instead of drawing it; dropping the same file there restores it. The row's readout opens a preview of the first rows as they parsed. `adults = person[person.age >= 18]` names a filtered copy of a file (see Filters above). Size: a column and constant arithmetic over it (`col/2`, `sin(col)`, `(a, b)`, reductions, `hist`) stay a single array and plot to 200 000 rows; combining a column with t, a slider, or a comparison needs one expression per row, and a row may build 100 000 of those in total — so `col t` reaches 100 000 rows (and so does `sin(col t) + 1`: a chain of operations over one list is built once, not once per operation). A three-column scatter (`(p.x, p.y, p.z)`) draws in 3D, where each point is a sprite, so it is capped at 10 000 and says so rather than drawing part of the file — filter it first, or plot two of the columns. - Naming a column, or arithmetic over one, names a list: `ages = person.age / 2`, `who = person.name`. It stays a single array, so rows below it (`mean(ages)`, `(ages, person.height)`) cost no more than reading the column directly. - List: `L = [3, 1, 4]` or `L = [1..100]` names a data list; rows below broadcast over it (`L^2`, `(L, L^2)`), reduce it (`mean(L)`), or filter it (`L[L > 2]`); to index it, sort it (`sort(L)[2]`, or `T = sort(L)`, then `T[2]`). Elements may use constants, states, t and i. A list of tuples names a scatter of points, whatever its shape: `[(1, 2), (3, 4)]` is two points, never a matrix. - Matrix: a tuple of rows, `M = ((a, b), (c, d))` or `M = ((a, b, c), (d, e, f), (g, h, i))`, or of named points as rows (`M = (A, B)`), is a 2×2 or 3×3 matrix; it works unnamed too: `((0, -1), (1, 0)) (2, 1)`, `det(((1, 2), (3, 4)))`. A list in an entry makes one matrix per element: with `a = [1, 2]`, `M = ((a, 0), (0, 1))` is two matrices, `det(M)` is `[1, 2]`, and `M M P` is two points (both Ms are the one M). `det(M)`, `trace(M)`, the matvec `M v`, and `solve(M, v)` (Cramer's rule) expand symbolically, so entries may hold sliders, t, x/y, or states, and results work anywhere: `(x', y') = A (x, y)` is a linear phase portrait, and `th' = om; om' = solve(M, f)` integrates a system written in the Lagrangian form M(θ)ω′ = f — the natural shape for a double pendulum. Matrices combine — `2 M`, `M / 2`, `M + N`, `M N`, `M^2` (whole powers 0–8), `M^-1`, `transpose(M)` — and a row may name the result (`R = M N`). A matrix has no position, so on a row of its own it draws nothing and reads out its value, `= ((1, 2), (3, 4))`: apply it to a vector to draw, or write `action(M)` to draw what it does — the image of the unit square (filled; its signed area is det M), of the unit circle (an ellipse whose axes are the singular vectors) and the arrows M e_x, M e_y, its columns; for a 3×3, the image of the unit cube and three arrows. `action(e^(t J))` turns the square with t. Over a list of matrices it draws each one's picture and reads each out: with `a = [1, 2, 3]`, `action(((a, 0), (0, 1)))` draws three stretches. A 2×2 matrix in x and y is a matrix field, `((x, y), (y, -x))`: a grid of glyphs, each the image of a small circle under the matrix there with a spoke to where e_x goes, drawn true size while small and a cell at most, in the row colour's complement where det < 0. `streamlines(M)` (or the row's `streamlines` toggle) draws it instead as streaks along the major eigenvector of its symmetric part (M + Mᵀ)/2 — a line field, with no arrowhead and no drift, fading at the degenerate points where that part is a multiple of I: `streamlines(hessian(x^3 - 3 x y^2))`. Note `a/2J`, touching, reads as `a/(2J)`; write `a/2 J`. - Tensors: a rank-k tensor is k-deep nested tuples, and its shape is part of its value — `((1, 2, 3), (4, 5, 6))` is 2×3, `(((1, 2), (3, 4)), ((5, 6), (7, 8)))` is 2×2×2. A value that is also a tuple of 2D or 3D points and not square draws as those points, however it was made — `(1, 2) ⊗ (3, 4, 5)` is the dots (3, 4, 5) and (6, 8, 10) — until a product reads it as a matrix: `T (1, 1)`. Operations act on coordinates: - `a ⊗ b` (`outer(a, b)`) is the outer product, `(a ⊗ b)_ij = a_i b_j`, for vectors, matrices or tensors: `e_x ⊗ e_y` is the 3×3 matrix with a 1 in row 1, column 2. - `contract(T, i, j)` sums T over its indices i and j (1-based, written as numbers), dropping both: `contract(M, 1, 2)` is `trace(M)`, and `contract(A ⊗ B, 2, 3)` is the product `A B`. Juxtaposition contracts the last index of the left with the first of the right, so `T v` and `A B` (2×3 by 3×2) work. - `a ∧ b` (`wedge(a, b)`) is `a ⊗ b − b ⊗ a`, a bivector, read out as its antisymmetric matrix; it is also a matrix, `(a ∧ b) v = a (b·v) − b (a·v)`. `e_x ∧ e_y ∧ e_z` is the rank-3 volume element (∧ is associative); in 3D `a × b` is the vector dual to `a ∧ b`. - `T[k]` of a named tensor is its k-th slice; `2 T`, `T + S` work entrywise. A tensor on its own row reads out; anywhere a number or point is needed it is an error until contracted. - Lists in entries work as for matrices: with `a = [1, 2]`, `a e_x ⊗ e_y` is two matrices, and with `M = ((a, 0), (0, 1))`, `M ⊗ M` is two rank-4 tensors, not four (both Ms are the one M). - `total`, `mean` and `count` take a multiset of tensors whole, entry by entry: with `P` a list of points, `C = mean((P - mean(P)) ⊗ (P - mean(P)))` is the covariance matrix, and `C (x, y)` applies it. - A bivector `a ∧ b` of 3D vectors, named or written out, generates the rotation in their plane: `e^(t a ∧ b) P`. - Multivectors (geometric algebra, Cl(2) and Cl(3), Euclidean): `a ⟑ b` (type `\gp`, or `gp(a, b)`) is the geometric product, `e_x ⟑ e_y` is the bivector `e_xy`, and `e_x ⟑ e_x` is 1. The blades are built in, like `e_x`: `e_xy`, `e_yz`, `e_zx` (and any reordering, `e_yx = −e_xy`) and `e_xyz`; they are always 3D, while `(1, 0) ⟑ (0, 1)` is the plane's own e_xy. Once one side is a multivector, juxtaposition is the geometric product too (`R p rev(R)`), `∧` is the outer product, and `+`, `−`, whole powers (`A^-1`, `A^3`) and `e^A` work; two plain vectors still need `⟑` (`e_x e_y` stays an error, `e_x ∧ e_y` the tensor wedge). Dividing by a multivector is refused — `a/2e_xy`, touching, would read as `a/(2e_xy)` — so write `a/2 e_xy`, and `A B^-1`. `rev(A)` reverses, `conj(A)` is the Clifford conjugate, `dual(A)` is `A I⁻¹` (`dual(e_xy)` is `e_z`), `grade(A, k)` is the grade-k part, and `|A|` its size. A row that is a multivector draws it grade by grade — its vector an arrow, its bivector an oriented disc of that area in its plane with an arrow round the rim, its trivector a cube of that volume — and reads out `= 1 + 2 e_xy`. A quaternion, or a rotor (a scalar and a bivector), draws as the rotation it makes: an arrow along the axis, |q| long, and a sector of radius |q| sweeping the angle 2 atan2(|v|, w), so its scalar part moves the picture. A result written only as a number or a vector is that number or vector (`grade(R ⟑ v ⟑ rev(R), 1)` is a point), and a row may name a multivector (`R = e^(-(t/2) e_xy)`). Rotors: `e^(-(th/2) B)` for a unit bivector B turns by th in B's plane, `R p rev(R)` applies it, and `rotate(X, R)` turns a point, a list of points or a figure by it: `rotate(hull(P), R)`. - Quaternions: `quat(w, x, y, z)` (or `quat(w, v)` with v a 3D vector) is w + x i + y j + z k, read out that way. They are the even multivectors of space (i = e_zy, j = e_xz, k = e_yx), so they multiply by juxtaposition (`quat(0,1,0,0) quat(0,0,1,0)` is `k`), invert with `q^-1`, conjugate with `conj(q)`, and the unit quaternion `quat(cos(th/2), sin(th/2) n)` turns by th about the unit axis n: `rotate(P, q)`. `grade(q, 0)` is its w, and `vec(q)` its vector part (x, y, z), a point. `slerp(q1, q2, s)` runs the shorter arc between two rotations at constant speed, s from 0 to 1: `rotate(hull(P), slerp(q1, q2, (1 - cos(t))/2))` rocks a solid between two orientations. A quaternion passed to a recursive function's self-call spreads into four parameters, so an orbit is written in quaternion algebra — the quaternion Julia set of C, sliced at k = s (see "Recursive function"): `C = quat(-0.2, 0.8, 0, 0); s = 0; Q(a, b, c, d, k) = {|quat(a, b, c, d)| > 2: ln(|quat(a, b, c, d)|)/2^k, k >= 12: ln(|quat(a, b, c, d)|)/2^k, Q(quat(a, b, c, d)^2 + C, k + 1)}; Q(x, y, z, s, 0) = 0.001`, framed with `camera(-pi/3, 0.5, 3)`. Older links write the same surface as `qjulia(C, s)`; it still draws, but new graphs should spell the orbit out as above. - Rotations are matrix exponentials: with `J = ((0, -1), (1, 0))`, `e^(a J) P` (or `exp(a J) P`) turns P by a, and in 3D `e^(a cross(n)) P` turns P by a about the axis n — `cross(n)` alone is the matrix of `v -> n × v`, scaled so |n| multiplies the angle (`rotate(P, a, n)` normalizes n for you). Both expand to exact cos/sin closed forms, so a may be a slider, t, or a LIST: `th = 2pi sort([0..4])/5; polygon(e^(th J) (1, 0))` is a regular pentagon (the turns sorted into a tuple, so the polygon has an order), `segment((0,0), rotate((1,0), th))` its 5 spokes, and `phi = (1+sqrt(5))/2; k = 2pi [0..2]/3; rotate((0, [-1,1], [-phi,phi]), k, (1,1,1))` the 12 vertices of an icosahedron (independent lists cross, the named turn list zips). `e^M` works for ANY 2×2 — `e^(t A) (x0, y0)` is the exact solution of `(x', y') = A (x, y)`, spiral, saddle or node — but a 3×3 must be a rotation generator (skew-symmetric). Name states for what they are (`th`/`om` for angle and angular velocity, `pos`/`vel` for a spring — the plane's own x/y/z are reserved). A derivative may use t, constants and other states (not x/y); an initial value must be constant, and gets a slider that relaunches the run when dragged. A ↻ button in the panel restarts it. - Coordinate field: a definition that depends on the plane, e.g. `r = sqrt(x^2+y^2); theta = atan2(y,x)`. Its level sets draw as a grid, and later rows may use it: adding `r = 2(1 + cos(theta))` plots a cardioid in polar coordinates. Here r is a distance, so a curve draws only where it is >= 0: `r = sin(4 theta)` shows 4 of the rose's 8 petals. For a polar curve that goes negative, trace it with theta = 2pi u instead: `(sin(8pi u) cos(2pi u), sin(8pi u) sin(2pi u))` is the whole 8-petal rose. A named pair or triple that uses x, y, or z is a vector field (see Point), not a grid. A field may be complex: `q = (x + i y)/2` (or `q = w^2`) is used wherever w is — `ln(q-2) - ln(q+2)`, `domain(q^3 - 1)`, `iter(z^2 + q)` — and draws no grid of its own. - Coordinate fields in space: a field may use z, e.g. spherical `rho = sqrt(x^2+y^2+z^2); theta = atan2(y,x); phi = acos(z/rho)`. A z-using field only defines — it draws no grid and does not by itself make the graph 3D; rows using it do: `rho = 2` is a sphere, `phi = pi/4` a cone, `rho = 1 + cos(3 theta)` a surface. Fields may be built from fields (`rho = sqrt(r^2 + z^2)`). Cylindrical needs no new field: r, theta and z. Angle surfaces cut at the branch of atan2: `theta = pi/4` is a half-plane, and a surface lying exactly on the cut (`theta = pi`) does not draw in 3D. `(rho, theta, phi) = (2, pi/4, pi/3)` is the point with those three coordinates (three coordinates in space, two in the plane; with a z-using field, two constraints trace a space curve). Only atan2-valued coordinates wrap modulo 2pi — an acos-valued polar angle lives in [0, pi] and does not. Where a chart is singular (theta on the z-axis) no point is reported. `(rho, theta, phi) = (2, 6 pi u, pi u)` traces a space curve in the chart. Points in space are not draggable. Three primed coordinates define a 3D chart flow, e.g. `(rho',theta',phi') = (0,1,0)`, transformed by the symbolic 3×3 Jacobian and drawn with trajectories or arrow glyphs. - Coordinate points: after defining r and theta above, `(r, theta) = (2, pi/4)` draws the point satisfying both coordinates. Angles in atan2-valued left components wrap modulo 2pi, so 9pi/4 and pi/4 name the same position. Bare tuples remain Cartesian. Non-injective charts draw multiple points: `p = x y; q = (x^2-y^2)/2; (p, q) = (1, 0)` draws two. In 2D, RHS literals or slider names can be dragged; the pointer position is evaluated in the named coordinates and written back to those values. - Parametric systems: `(r, theta) = (3u, 6pi u)` traces a three-turn spiral, u in [0,1]. General square systems with u work too, including in 3D. A bounded numerical continuation follows branches, periodically searches for others, and breaks lines at missing/discontinuous solutions. Small branches can be missed; this is not certified exhaustive tracing. - Coordinate flows: `(r', theta') = (r(1-r), 1)` draws a polar limit cycle. Both distinct coordinate names must have primes. Velocities are transformed through the coordinate Jacobian; singular chart locations are undefined. In 2D, click to trace trajectories just as for Cartesian vector fields. A lone prime on a field is invalid; an otherwise unbound name still defines a state. - Surface intersections: `(x^2+y^2+z^2,z) = (9,1)` traces the circle where the sphere meets the plane. Bounded predictor–corrector continuation follows the Jacobian null space, with at most 24 branches and 512 steps per direction. Singularities stop a trace; tiny or isolated branches may be missed. This numerical tracing does not certify branch completeness. - Certified point solving: a square system's “certify search box” control runs outward-rounded interval subdivision and Krawczyk tests in the worker. Its readout distinguishes proved roots, unresolved regions, and completeness of that finite search box. Existing numeric points remain visible. Certification currently supports finite constants, +, −, ×, / away from zero denominators, and integer powers up to 32; other functions, singular roots and exhausted budgets remain unresolved. Wrapped angular systems are excluded. Proofs concern real arithmetic on the input binary64 literals and parameter values, not a claim of completeness over all space. ## Viewport rows (framing) - `view(x = -10..10, y = -1..1, ratio = 5)` uses five times as many pixels per y unit as per x unit. `ratio` must be positive and defaults to 1; bounds are fitted while preserving this ratio across screen sizes. Option/Alt + drag scales axes independently and creates/updates this row. Set `ratio = 1` to restore equal axis units. - `view(x = -5..5, y = -2..2)` sets the 2D window; either axis alone works (`view(x = 0..0.01)`), the other is centered at 0 and sized by the canvas aspect. The whole box is always visible (uniform scale). - A log axis is a map from the screen: `view(x = 1..1000, x = 10^X)` makes x equal 10^X, where X is the panel's linear screen coordinate (Y for y), so `view(x = 0.1..100, y = 0.01..10000, x = 10^X, y = 10^Y)` is log-log and a power law draws as a straight line. The window stays in x units. Any increasing map built from exp, ln, log, powers, sqrt, sinh and arithmetic works (`y = sinh(Y)` is a symlog axis). A map may use sliders: with `b = 10`, `x = b^X` is a log axis whose base slides, and the window keeps its x range as b moves. It applies to its own panel only. The grid and its labels follow the map: decades on a log axis (1, 2, 3, 5 between them when zoomed in), and a zero line where the map reaches 0. Graphs, implicit curves, regions and fields are drawn through the map; points, parametric curves and regions, segments and polygons (bent as a straight line in x and y is on a log axis), point lists and labels are placed through it, and named points drag in x and y. Real systems are solved through it too (not certified or dragged there); a complex system like `w^3 = 8i` is solved in x and y and its roots placed. Hovering shows local minima and maxima and any intercepts with x = 0 or y = 0 the map shows, read in x and y (no inflection points), and traces curves in x and y. What the map cannot show (x <= 0 on a log axis) is left out. 2D vector fields, slope fields and `(x', y') = (P, Q)` flows are carried through it too: their arrows, streamlines and traced trajectories follow x and y (Lotka-Volterra's orbits close on log-log axes), coordinate flows like `(r', theta') = …` included. A 2×2 matrix field's glyphs are the matrix as it acts on the screen, and its streamlines follow its major eigenvector in x and y. Histogram bars and a definite integral's shaded area stand on y = 0, or rise from the bottom edge of a log y axis, which cannot show y = 0. 3D is not drawn on mapped axes, and a mapped panel shows no coordinate-field grid. - A plane map writes x and y together from the screen's X and Y: `view((x, y) = (Y cos(X), Y sin(X)), X = -pi..pi, Y = 0..5)` unrolls the plane in polar form — angle across, radius up — so circles about the origin are level lines and spirals rise in steps; `(x, y) = (exp(Y) cos(X), exp(Y) sin(X))` is log-polar, where equiangular spirals are straight. The window is the screen's, X and Y (not x and y). Any smooth map works, with sliders, and sums or products over fixed bounds (`sum(n=1..3, Y^n/n)`). Write functions with parentheses: `cos(X)`, not `cos X`. Every row is drawn through it: curves, regions, fields, flows and matrix fields (carried by its Jacobian), real systems, and points, parametric curves, figures, labels and complex roots, placed wherever the screen shows them (every copy, on an angle range wider than 2π; a line picks up at the other edge where it crosses the seam; a shape round the origin unrolls into an open curve and is filled down to Y = 0). Named points drag in x and y, and hovering shows a curve's intercepts and extrema in x and y where the screen shows them. The grid is the screen's; `grid(x, y)` or coordinate fields (r, theta) draw their level lines through the map. Histograms are refused, and integrals are readouts without shading. - `on((X, Y, Z) = (3cos(y) cos(x), 3cos(y) sin(x), 3sin(y)), x = -pi..pi, y = -pi/2..pi/2)` draws its panel's 2D rows on a surface in space — here the sphere, x as longitude and y as latitude. The panel is 3D (frame it with `camera(…)`, not `view(…)`). Curves and inequalities in x and y (and families of them) and scalar fields are painted on the surface per pixel; points, parametric curves, point lists, labels, figures and geodesics (`geodesic(P, (1, 0))`) are carried onto it, figures as outlines (paint an area with an inequality); a parametric region `(u cos(2 pi v)/2, u sin(2 pi v)/2)` is carried whole, as a fill. A vector field `(-y, x)` draws as a lattice of direction arrows tangent to the surface (the field (u, v) at (x, y) is the tangent u ∂P/∂x + v ∂P/∂y), each about a lattice cell long. 3D rows draw as usual; other 2D rows (e.g. matrix fields, complex rows, colour fields, histograms) are refused. Hovering the surface reads x and y there, with scalar and vector fields' values; near a painted curve it traces it ("on curve"), near a point it reads that point; named points drag along the surface. Any smooth surface works, with sliders and sums over fixed bounds: a torus is `on((X, Y, Z) = ((3 + cos(y)) cos(x), (3 + cos(y)) sin(x), sin(y)), x = -pi..pi, y = -pi..pi)`. - `camera(theta, phi, radius, (tx, ty, tz))` aims the 3D orbit camera — angles in radians (`camera(-pi/3, 0.6)`), radius and target optional. A trailing `spin = rate` keeps it orbiting about the vertical axis at that many radians per second (`camera(-pi/3, 0.6, spin = 0.3)`; negative turns the other way). theta is where the orbit starts: the row stays put while the view turns, and dragging pauses the spin. - Binding is two-way in the app: panning, zooming, or orbiting rewrites the row the way dragging a slider rewrites its constant, so the URL always names the exact picture on screen. Without a viewport row, interaction stays ephemeral and the default framing applies. - Bounds and angles may use `pi` and defined constants, evaluated at load. - A trailing `locked` pins a window or camera (`view(x = -1..1, y = -1..1, locked)`, `camera(0.4, 0.3, 8, locked)`): dragging and zooming leave it where the row puts it. ## Split views (panels) A row of three or more dashes, `---`, splits the graph into panels. Rows below it draw in a new panel with its own x, y and z, its own 2D-or-3D mode, and its own `view(…)`, `camera(…)` and `grid(…)` rows. Definitions (sliders, `f(x) = …`, states) are shared by every panel. Dragging pans or orbits the panel under the pointer. - Placement follows the dashes: `--- below`, `--- above`, `--- right`, `--- left`, or `--- inset top left` (picture in picture, floating in a corner of the panel above; corners: top/bottom with left/right, default top right). A bare `---` splits along the longer side. - A size gives the new panel's share: `--- below 30%` (default 50%; an inset defaults to 35% of its host's width and height). - Each split divides the most recent tiled panel, so a 3D scene with two 2D panels stacked on its right is `--- right 40%` then `--- below`. - `shared x` locks the panel's x axis to the panel it split from: same center and scale, so stacked plots line up column for column, and panning either pans both. Its `view(y = lo..hi)` then frames y alone. `shared y` does the same for rows (side-by-side panels at the same height). - A function over its derivative: `f(x) = x^3 - 3x; y = f(x); --- below 40%, shared x; view(y = -4..10); y = d/dx f(x)` - A 3D surface beside its contour map: `z = sin(x) cos(y); --- right; view(x = -4..4, y = -4..4); sin(x) cos(y)` - At most 8 panels. Options: order is free, commas optional (`--- below 40%, shared x`). ## Grid rows `grid(…)` sets what draws behind its panel's plots (one per panel): - `grid(off)` — nothing: no grid lines, axes or numbers (a pendulum, a diagram). In 3D it hides the axes and the floor grid. - `grid(axes)` — just the two axes and their numbers. - `grid(r, theta)` — exactly these coordinates' level sets, e.g. after `r = sqrt(x^2+y^2); theta = atan2(y,x)`. `grid(x, y)` keeps the Cartesian grid even where coordinate fields are defined. - Without a grid row, coordinate fields defined in a panel replace that panel's Cartesian grid (so `grid(x, y)` is how to define r and theta for later rows without drawing them). ## Guidance for assistants - To show a user a graph, emit a single https://equation.io/g/... link built as above; the graph renders immediately on load, no interaction needed. - To edit a graph the user already has, decode their link (or call the MCP `decode_graph_url` tool), transform the rows, and emit a new link. Prefer restructuring into named definitions: e.g. given `y = x^2 - 2x`, "add a tangent line at x = 3" becomes `f(x) = x^2 - 2x; g(x) = d/dx f(x); a = 3; y = f(x); y = f(a) + g(a)(x - a)` — with `a` as a draggable slider so the point of tangency stays explorable. - Prefer a slider constant (`a = 1; ...`) when the user may want to explore a parameter, and `t` when they ask for animation. - Don't name a slider, list or index `i`, `e`, `pi` or `d`: they are taken (`i` is the imaginary unit), so `i = [0..9]` is a claim about i, not a definition, and every later `i` stays complex. Use `k = [0..9]`. - The graph state lives entirely in the link, and the link is the interface. The only server endpoint is /mcp (an MCP server that validates equations and emits links). - If you are unsure whether a form is supported, emit the link anyway. Unsupported input fails loudly and visibly (an inline error on the offending row), never silently — so a graph either renders correctly or tells the user what to fix. There is no wrong-but-silent output to worry about. ## More - [App](https://equation.io/): the grapher itself - [About](https://equation.io/about/): feature gallery with screenshots - [Source code](https://github.com/aantthony/equation.io): open source (MIT); bug reports via GitHub issues - [robots.txt](https://equation.io/robots.txt): crawling policy ## Regression Write `P = [(0,1),(1,3),(2,5),(3,7)]; P.y ~ m P.x + b` to fit a line through the points: `P.x` and `P.y` are their coordinates, paired point by point. (Write the data as points, or as columns of one CSV file: two separately defined lists `X = […]`, `Y = […]` are independent, so `(X, Y)` would plot their grid.) The left side is a numeric list (defined above the fit), an explicit list, or a CSV column. The right side is a model over equally long lists, or a scalar broadcast to the observations. Unbound scalar names are fitted coefficients; already defined constants stay fixed. Data and fixed constants must be static and defined above the fit. Coefficients are reusable values: `y = m x + b` draws the model, `f(x) = m x + b` names it, and `(P.x, P.y - f(P.x))` draws residuals. No fit settings exist outside the equations. Examples: `P.y ~ a P.x^2 + b P.x + c` (polynomial), `P.y ~ a exp(b P.x)` (nonlinear), `data.height ~ m data.age + b` (CSV). A fit row reports coefficients, RMSE, R² when the observations have nonzero variance, and observation count. Changing fixed constants or data refits the model. Missing/nonfinite pairs are skipped with a visible count. Unequal lengths, rank-deficient models, and too few finite observations are errors. At most 8 coefficients and 10,000 observations, or 2,000 observations for nonlinear models. Nonlinear models must be differentiable in fitted coefficients; bounded, deterministic multistart fitting reports a local fit or an error, not a guaranteed global optimum. Fitting does not depend on t or simulation states. `X ~ Normal(...)`, `X ~ Uniform(...)`, `X ~ Exponential(...)`, and the unambiguous family names (`ChiSquared`, `StudentT`, `LogNormal`, `Cauchy`, `Weibull`) retain their random-variable meanings. `Gamma`, `Beta` and `T` follow the `exp` rule below — over a declared data list they are a model (`Y ~ gamma(a X)`, `Y ~ beta (X - 1)`); with an undeclared left-hand name they declare a random variable. Bare distribution aliases are case-insensitive, including `exp`/`EXP`. For a declared data list, `Y ~ exp(a X)` is an exponential regression model in any capitalization; with an undeclared left-hand name, `Z ~ exp(2)` declares an exponential random variable. Missing CSV data stays device-local in MCP, including curves that depend on its fitted coefficients. Fits over literal data and their model curves work in shared links. ## Labels `label(point, "text")` writes text beside a point on the graph, in the row's color. Use it as a whole row. Examples: label((2, 4), "the curves cross here") A = (1, 1) label(A, "vertex") a = 2 label((a, a^2), "moves with the slider") The point may be a tuple, a named point, or point arithmetic, in 2D or 3D, and may use constants, sliders, states, and `t`; it cannot use free x/y/z or u/v. The text is plain and fixed. The MCP classifies these rows as `label`. ## Motion trails `trail(A)` leaves a live trail of a point's observed positions, with a dot at its current position. Use it as a whole row. Examples: A = (cos(t), sin(t)) trail(A) trail((cos(t), sin(t), t/5)) A named 2D point, point arithmetic such as `trail(2A)`, or a 2D/3D vector state can be traced. Components may use constants, sliders, states, and `t`; they cannot use free x/y/z or u/v. `trail(cos(t), sin(t))` is equivalent to the parenthesized tuple spelling. Matrix `trace(M)` keeps its existing meaning (the sum of the diagonal), including inside other expressions. The live browser retains at most 30 seconds and 2048 samples per trail, sampling at most 60 times a second. Undefined positions or pauses over half a second break the line instead of joining unobserved motion. Editing equations or restarting the simulation clears history; framing/comment edits preserve it. Trails start afresh on opening a graph and are not saved in shared URLs. The MCP classifies these rows as `trail`.