edit

Cells

A cell is an optionally named program whose value or value stream can be referenced by other cells.

source defines
42 an anonymous cell
answer = 42 answer
total = { … return v; } total, from a block body
settings = ({a: 1}) settings. Without the parens { opens a block and the value is undefined
viewof x = element viewof x and x
mutable y = 0 mutable y and y
import {a} from "@user/nb" a
edit
answer = 6 * 7
edit
answer = 42
edit
sum_to_ten = {
  let total = 0;
  for (let i = 1; i <= 10; i++) total += i;
  return total;
}
edit
sum_to_ten = 55
edit
settings = ({ retries: 3, timeout: 1000 })
edit
settings = Object {retries: 3, timeout: 1000}
edit

Dataflow

A cell's inputs are the identifiers in its body. When the value of an input changes, the cell is re-run by the Observable Runtime.

edit
viewof celsius = Inputs.range([-40, 100], {value: 20, step: 1, label: "°C"})
edit
edit
fahrenheit = celsius * 9 / 5 + 32
edit
fahrenheit = 68
edit

this is the cell's previous value, so a cell can fold over its own executions. reruns has one input, so it counts how often the slider has caused recomputation.

edit
reruns = {
  celsius;
  return (this ?? 0) + 1;
}
edit
reruns = 1
edit

What the compiler sees

Cells are mapped to one or more reactive variables. The @tomlarkworthy/observablejs-toolchain is able to map to and from the lower-level reactive variable representation. Reactive variables have an inputs array, a definition and an optional name.

edit
edit
edit
nameinputsdefinition
fahrenheitcelsiusfunction _fahrenheit(celsius) {return (celsius * 9 / 5 + 32);}
edit

A cell that fails to parse compiles to a SyntaxError holding its _sourceExpression, so even broken cells can be decompiled back to their original.

edit

viewof

viewof name = element is a special construction designed for UI widgets. A viewof defines two cells: viewof name is the UI element, and name is element.value, which emits a new value every time the UI changes (specifically triggered by an Event('input') from the UI element).

Inputs is a library of these, but there is nothing special about them. viewof point below is hand-written.

edit
viewof fruit = Inputs.select(["🍎", "🍌", "🍇"], {label: "fruit"})
edit
edit

fruit is 🍎

edit
viewof point = {
  const el = htl.html`<svg width="240" height="120" style="border:1px solid var(--theme-foreground-faint); touch-action:none">
    <circle r="6" fill="currentColor"></circle>
  </svg>`;
  const dot = el.querySelector("circle");
  const set = (x, y) => {
    el.value = {x: Math.round(x), y: Math.round(y)};
    dot.setAttribute("cx", el.value.x);
    dot.setAttribute("cy", el.value.y);
    el.dispatchEvent(new Event("input", {bubbles: true}));
  };
  el.onpointermove = (event) => {
    if (event.buttons === 0) return;
    const box = el.getBoundingClientRect();
    set(event.clientX - box.left, event.clientY - box.top);
  };
  set(120, 60);
  return el;
}
edit
edit

drag in the box — point is 120, 60

edit

mutable

mutable name = value defines a cell that other cells can write into.

edit
mutable clicks = 0
edit
clicks = 0
edit
clicks_button = {
  const button = htl.html`<button type="button">+1</button>`;
  button.onclick = () => ++mutable clicks;
  return button;
}
edit
edit

clicks is 0

edit

Promises and generators

Cells can return a promise and the Runtime will wait for it before triggering downstream cells.

Cells can also emit streams of values by yielding. The internal representation is then a generator and the runtime pulls one value per animation frame, re-running downstream cells each time.

Descendants of a fast generator miss values: the graph converges on the latest state rather than processing a stream (responsiveness).

edit
later = Promises.delay(1000, "…arrived a second late")
edit
later = "…arrived a second late"
edit
viewof clock_running = Inputs.toggle({label: "run the clock", value: true})
edit
edit
clock = {
  if (!clock_running) {
    yield "paused";
    return;
  }
  while (true) {
    yield new Date().toLocaleTimeString();
    await Promises.delay(1000);
  }
}
edit
clock = "12:01:17 PM"
edit

invalidation is a promise that resolves when the cell is recomputed, useful for undoing side effects or releasing resources. Timers, sockets and listeners have to be released there or they outlive the cell. Delete the .then below and the setInterval keeps counting until the page closes.

edit
invalidation_example = {
  const el = htl.html`<span>0s</span>`;
  let seconds = 0;
  const timer = setInterval(() => (el.textContent = `${++seconds}s`), 1000);
  invalidation.then(() => clearInterval(timer));
  return el;
}
edit
39s
edit

Standard library

Certain functions are available to all modules without importing them. The bootloader sets them up before other modules load.

edit
namekindwhat
mdtagMarkdown, as in every prose cell here
htmltagHTML fragment
svgtagSVG fragment
textagTeX, rendered by KaTeX
htlobjecthtml / svg templates that escape interpolations
InputsobjectThe widget library
PlotobjectObservable Plot
d3objectD3
FileAttachmentfunctionA file bundled with the notebook
Generatorsobjectinput, observe, queue, range — generators over events
Promisesobjectdelay, tick, when
MutableclassThe box behind mutable cells
nowgeneratorThe current time, once per frame
widthgeneratorThe width of the cell's container
invalidationpromiseResolves when this cell is replaced
visibilityfunctionResolves when the cell is on screen
DOMobjectcontext2d, svg, uid and other DOM helpers
requirefunctionAMD loader for npm modules (legacy)
edit

import

import {a, b} from "@user/notebook"                  // by name
import {a as alias} from "@user/notebook"            // renamed
import {viewof v, mutable m} from "@user/notebook"   // views and mutables
import {chart} with {sales as data} from "@user/nb"  // injection

Import cells bring references from other modules into the module's scope. These are live reactive references.

edit

linkTo, imported from @tomlarkworthy/lopepage-urls, turns a module and cell into a lopepage link: Inputs.range.

edit

Errors

An error stays in the cell that raised it. Cells downstream report the same error, and do not recompute. Errors can be used to deliberately stop reactive propagation.

message cause
x is not defined nothing defines x: no cell, no import, no builtin
x is defined more than once two cells claim the name
circular definition a cell's inputs lead back to itself
a SyntaxError when the cell runs the body did not parse
a cell pending forever an await or a generator that never resolves
edit

Utility: printing a cell's source

lookupVariable finds a cell's runtime variables by name and decompile turns them back into source. This is just used here to help drive the examples on this page.

close
inputs: md
reflection_docs = md`## Utility: printing a cell's source

\`lookupVariable\` finds a cell's runtime variables by name and \`decompile\` turns them back into source. This is just used here to help drive the examples on this page.`
show = {
  liveCellMap; // an input only so that editing any cell re-prints every source below
  const group = (name) =>
    name.startsWith("viewof ")
      ? [name, name.slice(7)]
      : name.startsWith("mutable ")
      ? ["initial " + name.slice(8), name, name.slice(8)]
      : [name];
  // observablehq.com's own compiler names these viewof x / mutable x; print the source spelling
  const unmangle = (src) =>
    src
      .replace(/\bmutable\$([A-Za-z_$][\w$]*)\.value\b/g, "mutable $1")
      .replace(/\b(viewof|mutable)\$([A-Za-z_$][\w$]*)/g, "$1 $2");
  return async function show(name) {
    if (!refModule) return md`\`${name}\` — module not resolved yet`;
    const variables = await lookupVariable(group(name), refModule);
    if (variables.some((v) => !v)) return md`\`${name}\` — no such cell`;
    return md`\`\`\`js
${unmangle(await decompile(variables))}
\`\`\``;
  };
}
edit
show = async ƒ(name)
edit
viewof refModule = EventTarget {tag: Symbol()}
edit
test_show_prints_live_source = "ok"
edit
test_compile_reads_inputs = "ok"
edit
test_viewof_compiles_to_two_cells = "ok"
edit
test_dataflow_recomputes = "ok"
edit
test_mutable_is_a_number = "ok"
edit
test_show_prints_a_viewof_cell = "ok"
edit
test_show_prints_a_mutable_cell = "ok"
edit
test_mutable_compiles_to_three_cells = "ok"
edit
test_mutable_write_depends_on_the_box = "ok"
edit
test_import_compiles_to_a_module_and_a_name = "ok"
edit
test_syntax_error_compiles_to_a_throwing_body = "ok"
edit
test_broken_cell_decompiles_to_its_source = "ok"
edit
edit
edit
edit
edit
edit