Published
Modified

Reactive HTML notebooks

Before I start, why am I doing this?

I don't think HTML is being used enough as a platform for scientific publishing.

Instead, people will:
  1. First use an interactive notebook like Jupyter, RStudio, Pluto.jl or Observable to do data exploration, analysis and visualisation,
  2. Then move to publishing platforms like Typst, Overleaf, pure LaTeX, or a WYSIWYG editor to typeset their work.

I think self-contained HTML files can be used for both of these stages, and prevent a lot of faffing around with CLI tooling, CI steps, or 3rd-party platforms.

HTML's typesetting capabilities are well documented, but its capabilities as a platform for data exploration, analysis and visualisation are not.

I'll try and demonstrate these capabilities, literate programming style.
A computer displays an open book

Cells

First, we'll steal a trick from This page is a truly naked, brutalist html quine, and create a CSS class called echo that will display/reflect style and script elements inline.

Add in a font with built-in syntax highlighting and a contenteditable attribute and we have a basic code editor!

I stress that this style element is styling itself to be visible.

Try changing .echo's background-color!
Now we'll import the Observable standard library and the Observable runtime, and bind them to window. We'll define helper methods cell and observer that wrap some of the runtime API.
Now we'll declare a cell called counter that emits a number every second. The script's id attribute is the same as the name parameter passed to cell.

contenteditable

We can get contenteditable working by ensuring that whenever we blur a script element, we force its re-evaluation by building a clone of it.

Try changing the initial counter value i above to a much bigger number, and then defocus the script.
Now that we've created a our counter cell, we can create other cells that depend on it.
We'll import Hypertext Literal and use it to format the counter value. htl implements a full-blown HTML5 parser that performs automatic escaping and interpolation of non-serializable values, such as event listeners, style objects, and other DOM nodes.
We can still observe the output of a cell without needing to show its definition. Just don't add the echo class. This makes them useful as a rendering primitive. (There's a hidden cell above ^)



We can use cell values in more complex outputs. We'll import Observable Plot and use the counter value in a plot.
We should always store data in their own script elements, so that they can be easily referenced by other cells. Observe that cells can be declared in any order, such that earlier cells may depend on later cells.
A computer displays a graph in the upward direction

TeX, Markdown, Graphviz

We can return any type of DOM element from a cell.
In this case, the tex, md, and dot cells return span, table and svg elements respectively.

Try editing any of the following cells.
A computer with an open CD tray is surrounded by data

Cell status

We can also return a Promise, or throw an Error, from a cell. Observable's Inspector will apply an observablehq--running or observablehq--error class to the cell's outer div element respectively. We'll style them appropriately:

SQLite

I've hosted the Chinook sample database on my website at https://maxbo.me/chinook.db. Now we'll use a WASM-backed SQLite client to query it.

Try adding WHERE Milliseconds < 1000000 to the SQL query!

Python

The Pyodide CPython WASM distribution includes NumPy, Pandas, Matplotlib, scikit-learn, and Scipy. We'll rebuild the plot seen above, but using Matplotlib and Python's sqlite3 module instead.

Try editing one of the plot labels!

R

You know the drill. It's R, using WebR. I didn't figure out how to get ggplot2 rendering working, but I assume it's possible. I must disclose that this cell seems to be a bit flaky on iOS. I have not had a chance to investigate further, nor will I.

Inputs

We'll create a new cell type viewof that works specifically with Observable Inputs. It declares 2 reactive cells: NAME and viewof NAME - one for the value, and one for the DOM element itself.
To display the input above the cell, we set the cell id to viewof NAME.

Wiggle the range input and see another dependent cell update.
NB: The way Observable Inputs work is a bit arcane. This demo of Synchronized Inputs may shed some light.

Mutability

Purely functional dataflow is great, but sometimes you just need to mutate state. We'll create a new helper function mutable. It registers a Mutable - an object that yields new Generator values when the value is mutated - in the runtime.
Try editing the initial state of the mutable. Try editing the button labels.

What's next?

I will try and cram all of this into a library with some proper documentation.
I initially thought it should be called incel (short for inline cell), but I'll probably call it celine instead.

I've released a library! It's called @celine/celine!
A computer terminal receives text

Slide infrastructure

I demo'd this article at SydJS. This is the code I used to turn the article into a slideshow.