6 Interactive and explorable documents
Documents a reader can operate: reactive js cells, inputs without boilerplate, and 3-D.
Taliesin runs {js} cells in the browser and wires them into a small reactive
graph. On top of that graph sits one declarative convenience, inputs. Everything
here is client-side and offline, built from the same block model as the rest of
the page, and the preview stays read-only: a reader interacts with the output,
never the source.
The fenced examples below are source listings; the {{< input >}} section runs live
on this page.
6.1 Reactive {js} cells
A {js} cell declares its place in a dependency graph with leading //|
directives (they must come before any code):
//| viewof: NAMEmakes the cell an input control published underNAME.//| name: NAMEpublishes the cell’s return value underNAME.//| input: A, Bconsumes those names; the cell re-runs when any of them change.
Inside a cell, read values through the injected tali API: tali.value("k") returns
a control’s current value, and tali.get("squared") returns the last value a
//| name cell published. A first cell publishes a slider as n:
//| viewof: n
const input = document.;
input. = "range"; input. = "1"; input. = "12"; input. = "3";
return input;
A second derives a value from it, and a third consumes that derived value, so the
chain n to squared to the readout stays in sync transitively:
//| name: squared
//| input: n
return tali. ** 2;
//| input: squared
return document.;
When an input changes, Taliesin re-runs exactly the cells downstream of it, once each,
in dependency order, following //| name edges; a cycle is reported as an error rather
than run. Plot and d3 are available as globals for drawing, and a cell either
returns a DOM node (mounted for you) or builds into tali.container.
The node a cell returns is mounted after the cell body finishes, so a guard like
if (!node.isConnected) return; inside the body always fires and the cell never paints.
An init gated on offsetWidth or getBoundingClientRect() is the same mistake.
invalidation is the signal that works: it is a promise resolving when the cell is
about to re-run or leave the page, whether or not the node ever reached the DOM, so it
is where you cancel animation frames and dispose GPU resources. If you genuinely need
post-mount measurements, take them inside a requestAnimationFrame callback.
A viewof input doesn’t need to flow through a //| name cell first: a consumer can
list it directly in //| input:.
//| input: n
return document.;
6.2 Inputs without boilerplate: {{< input >}}
The {{< input >}} shortcode emits a labelled, keyboard-accessible control whose
value becomes a reactive node, so a {js} cell reacts with //| input: and no
control-building code. Five of them are ordinary form fields:
| Argument | Required | Default | Effect |
|---|---|---|---|
name= | warns if omitted | (none) | The reactive node’s name (what a consuming cell reads). Omitting it warns (the control still renders, but it can’t feed the reactive graph). |
type= | no | slider | One of slider, number, checkbox, text, select. An unknown type warns with a did-you-mean. |
label= | no | the name | The visible label text. |
value= | no | (none) | The initial value. A checkbox is pre-checked only when value is the literal true; a select’s value must exactly string-match one of the options to pre-select it. |
min= max= step= | no | (none) | Numeric bounds for slider and number. |
options= | for select | (none) | A comma-separated option list. Required for type="select"; omitting it warns. |
tali.value() returns a number for slider and number, a boolean for
checkbox, and a string for text and select. A consuming cell is then just a
{js} cell with //| input: k that reads tali.value("k").
A slider renders as HTML’s <input type="range"> with a live <output> readout
that updates as you drag, so the reader always sees the current value beside the track.
number shares its numeric min/max/step/value attributes but renders as a spinner
with no readout.
Here is the first shortcode of that block running live, with the consuming cell under it. Drag the slider and the curve redraws.
6.3 Interactive 3-D with a {js} cell
Any node a cell returns is mounted, and that node can be a WebGL canvas, so a full
interactive 3-D scene is just a cell that imports a 3-D library and returns its
renderer’s canvas. Three things to get right: construct the renderer with
alpha: true, or its clear colour paints an opaque slab over the page in one theme or
the other; cancel animation frames and dispose GPU resources on invalidation; and
note that a library fetched over the network (three.js from a CDN, say) is your own
trade against the offline default, so such a page needs http(s), not file://.
6.4 The Python → JS bridge
define() in a Python cell hands values to {js} cells, so you can compute
in Python and render reactively in the browser. Its signature is keyword-only,
define(name=value, ...): each keyword becomes a published name, and each
value is JSON-serialized into the page for the {js} runtime to pick up. A {js}
cell then reads the value as tali.defines.NAME (or tali.value("NAME"), which
falls back to defines when no live input owns that name). In live preview the
Python cell executes after the page loads, so its values arrive a moment after
the {js} cells; a {js} cell reads them and re-runs automatically when they land
(and rebinds when a Python input changes), so the cell recomputes in place without
a reload. Guard against the value not having arrived yet with an early return.
# sample a noisy sine wave
=
const signal = tali..;
return;
return Plot.;