Glossary and mental model#

Hooks are much easier to debug if everyone uses the same words for the same things. This page defines the key terms and gives you a “one diagram” mental model.

Glossary (terms used in this chapter)#

Cellucid (web app)#

The browser-based viewer UI. In notebooks, it runs inside a widget in the cell’s output.

cellucid-python (Python package)#

The helper package you use from Python/CLI. It can:

  • serve datasets (an AnnData store the viewer reads itself),

  • show the Cellucid UI in notebooks (a widget),

  • send commands to the UI and receive events back (hooks),

  • capture session files and apply them to AnnData.

Viewer / viewer#

The Python object returned by show_anndata(...): a cellucid.Widget.

It owns:

  • the dataset it answers the viewer’s reads from

  • a hooks registry (viewer.on_selection, etc.)

  • a small latest-state snapshot (viewer.state) and the viewer’s results (viewer.results, viewer.pull(adata))

A browser-tab viewer (AnnDataViewer) owns the same, plus a running background server (viewer.server_url) and the tab’s address (viewer.viewer_url).

Widget#

The notebook output that shows the viewer: an anywidget whose front end frames the Cellucid app and relays between it and the kernel over the notebook’s own connection (a Jupyter comm). No port, no server.

Data server#

A small local HTTP server, started by a browser-tab viewer (AnnDataViewer, serve_anndata, cellucid serve). It serves:

  • the AnnData store’s bytes, read by the viewer with range requests

  • the exact verified viewer generation

  • special endpoints like:

    • /_cellucid/health

    • /_cellucid/info

    • /_cellucid/protocol (where the viewer channel is, and what it carries)

    • /_cellucid/ws (the viewer channel, a WebSocket)

Viewer channel#

The one channel the viewer opens to Python: over the widget’s comm in a notebook, a WebSocket to the data server (/_cellucid/ws) in a browser tab. Both commands and events travel on it. See Architecture: the viewer channel.

Command (Python → viewer)#

A message Python sends to the viewer on the viewer channel. Examples:

  • highlight cells

  • set color-by

  • reset camera

Event (viewer → Python)#

A message the viewer sends to Python on the viewer channel. Examples:

  • selection

  • hover

  • click

  • ready

viewerId#

A unique ID naming one viewer instance (important when you have multiple viewers in one notebook): the viewer says it when it opens its channel, and Python binds the channel to that viewer.

viewerToken#

A per-viewer secret token: a widget hands it to its page in a message, a browser tab carries it in its address. The viewer sends it once, when it opens its channel, and Python compares it in constant time before the channel carries anything. A page without the token cannot open a channel, so no other page can send events as the viewer or receive its commands.

Origin, CORS, mixed content#

Browser security concepts that explain many failures:

  • Origin: scheme + host + port (e.g. https://example.com:443)

  • CORS: rules about which origins can make requests to which servers

  • Mixed content: browsers blocking https://... pages that try to load http://... resources

See Security: CORS, origins, and mixed content.

Verified local web generation#

A browser-tab server establishes the viewer UI assets declared by https://www.cellucid.com (verified, cached on disk) at every start and serves them from the same origin as the dataset. A notebook widget keeps none: the browser loads the app from web_source_url.

Controls (browser-tab viewers):

  • Generation location: web_cache_dir=...

  • Clear cache: cellucid.clear_web_cache() or viewer.clear_web_cache()

Mental model (one diagram)#

┌──────────────────────────────────────────────────────────────────────┐
│                            Your notebook                              │
│                                                                      │
│  Python kernel                                                       │
│   ├─ viewer = show_anndata(...)   (a cellucid.Widget)                │
│   ├─ answers the viewer's reads of the dataset                       │
│   └─ registers hooks: @viewer.on_selection / @viewer.on_hover / ...  │
│                                                                      │
│  Browser (notebook frontend)                                         │
│   └─ the widget's output runs the Cellucid web app                   │
│      (loaded from https://www.cellucid.com)                          │
│                                                                      │
│  The notebook's own connection carries one viewer channel:           │
│   Python → viewer: commands      viewer → Python: events             │
└──────────────────────────────────────────────────────────────────────┘

If “hooks don’t work”, it’s almost always timing or a stale output:

  • a cell is still running (the widget’s events arrive between cells)

  • the browser could not load the app (the output says so)

  • a stale output after a kernel restart (it keeps its last view and says the notebook stopped it)

Commands sent before the viewer is shown are not lost: they wait in Python and arrive, in order, when the viewer connects.

Common misconceptions (and the correct model)#

“Hooks are a Jupyter feature”#

Not quite. Hooks are messages on one viewer channel. In a notebook the channel rides on the widget’s comm (so any front end that shows widgets works); a browser tab carries the same channel on a WebSocket.

“If I can see the viewer, hooks must work”#

Not necessarily:

  • you can see the UI while a cell runs, but its events reach Python only when the cell ends;

  • in a browser tab behind a proxy, the proxy may refuse the WebSocket; the viewer then says it cannot open its channel and names the proxy.

“Cell indices are stable cell IDs”#

In the hooks API, indices are row positions (0-based). If you reorder/subset the dataset in Python, indices change.

“I can treat hover events like clicks”#

Hover is a debounced, high-frequency state stream rather than a discrete click record. Keep callbacks light and do not assume that every intermediate pointer position produces an event.

Next steps#