Skip to content
Taliesin User Guide

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: NAME makes the cell an input control published under NAME.
  • //| name: NAME publishes the cell’s return value under NAME.
  • //| input: A, B consumes 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.createElement("input");
input.type = "range"; input.min = "1"; input.max = "12"; input.value = "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.value("n") ** 2;
//| input: squared
return document.createTextNode("n squared = " + tali.get("squared"));

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.

Gate teardown on invalidation, never on DOM attachment

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.createTextNode("n = " + tali.value("n"));

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:

{{< input name="k" type="slider" min="1" max="10" step="1" value="3" >}}
{{< input name="n" type="number" min="0" max="100" value="20" label="n" >}}
{{< input name="on" type="checkbox" value="true" label="enabled" >}}
{{< input name="q" type="text" value="hello" label="query" >}}
{{< input name="c" type="select" options="red,green,blue" value="red" >}}
ArgumentRequiredDefaultEffect
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=nosliderOne of slider, number, checkbox, text, select. An unknown type warns with a did-you-mean.
label=nothe nameThe 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.

3

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
import math, random
random.seed(7)
signal = [math.sin(2 * math.pi * i / 40) + random.uniform(-0.3, 0.3)
          for i in range(120)]
define(signal=signal)
const signal = tali.defines.signal;
if (!signal) return;
return Plot.plot({
  height: 180, marginLeft: 36,
  marks: [Plot.ruleY([0]), Plot.lineY(signal, {stroke: "#3A5578"})]
});