Engineering notes

How the widget pages on this site are actually built, and the dead end that led there.

How a widget's front-end code gets into a static page

TrajectoryScrubber, MotilityCellPaintingView, and Celldega's Clustergram are all anywidget widgets: each one declares _esm = Path(...)/"static"/"foo.js", and anywidget reads that file's text content into the widget's serialized state at construction time — the actual JS source (our bundled deck.gl code, ~2.2MB per widget) travels as a plain string trait alongside the widget's data traits (frame_urls, trajectories, etc.). Once a notebook cell displaying the widget is executed and saved, that whole state — JS source included — is embedded in the .ipynb file itself, under metadata.widgets["application/vnd.jupyter.widget-state+json"]. Nothing needs to be published anywhere for this part; it's just a big JSON blob sitting in the notebook.

The dead end: classic ipywidgets static embedding doesn't work for anywidget

The obvious path is nbconvert's HTMLExporter or ipywidgets.embed.embed_minimal_html — both pull in @jupyter-widgets/html-manager, a requirejs/AMD-based renderer, from a CDN at page-load time. This works fine for classic ipywidgets controls (sliders, dropdowns), but fails for anywidget widgets specifically, and no amount of CDN version pinning fixes it. The reason: anywidget's real npm package (check its package.json) declares "type": "module" with "main": "dist/index.js" and no UMD/AMD build at all. Requirejs's classic define()/require() system cannot consume a pure ES module — the two module systems are fundamentally different, not a version-compatibility gap.

Symptom, if you hit this yourself: browser console shows strict mode: use allowUnionTypes to allow union type keyword (a red herring — that's just Ajv being noisy about the widget-state schema, harmless) followed by anywidget.js ... 404 and Falling back to https://cdn.jsdelivr.net/npm/ for anywidget@~0.9.* — the widget never actually renders after that fallback, regardless of network reachability or html-manager version.

The fix: anywidget's own portability spec (AFM)

anywidget defines the Anywidget Front-End Module (AFM) specification precisely for this situation — "widget reuse... beyond Jupyter, including... standalone web applications." A widget's _esm is just an ES module with initialize({model, signal}) and render({model, el, signal, host}) lifecycle hooks; a host platform just needs to implement a small AnyModel interface (get/set/on/ off/save_changes/send) and call those hooks. This site's host is site/vendor/afm_host.js — about 60 lines, no dependencies, no CDN calls at all. site/build_site.py's export_widget_page pulls the widget's _esm/_css and traits straight out of the executed notebook's saved state, writes them as {page}_widget.mjs / {page}_widget.css, and generates a small HTML page that imports the host and calls renderAFM(...).

Gotchas, if you extend this

No publishing needed

None of this requires publishing our widget JS to npm or the Python package to PyPI. anywidget itself (the generic host-bridge concept) is already a public package, but we don't consume it as a package at all in this approach — afm_host.js is a from-scratch, from-spec implementation, and every widget's actual JS ships as a plain file alongside the HTML that uses it, exactly like the rest of this static site.

← back to all results