Jupyter (notebook embedding + hooks)#

This page documents the notebook-facing APIs:

  • Notebook view: show_anndata() (an AnnData or MuData object, or any file or folder Cellucid reads: .h5ad, a Zarr store, .h5mu, .loom, a 10x .h5 or outs/ folder, a MatrixMarket folder, an .rds/.rda, or a file written by prepare())

  • Interactive control: the widget it returns (Widget) supports hooks + Python→frontend commands


Audience + prerequisites#

Audience

  • Wet lab / beginner: copy/paste the “Fast path” and use the troubleshooting section when something looks wrong.

  • Computational: use “Practical path” + “Edge cases” to avoid performance and data-shape pitfalls.

  • Power user: use “Deep path” to understand how the server + hooks + session capture work.

Prerequisites

  • pip install cellucid

  • If you use AnnData: install anndata (and typically numpy, scipy, pandas).

  • A notebook that shows widgets: JupyterLab, Jupyter Notebook 7, VS Code, Google Colab, marimo or Databricks (Supported environments matrix).


Fast path (beginner-friendly)#

A) “I have an AnnData”#

from cellucid import show_anndata

viewer = show_anndata(
    adata,
    dataset_name="My study",
    dataset_id="my-study-v1",
)

What you should see:

  • A Cellucid viewer in the notebook output, showing your embedding, once the cell has finished (the viewer reads the data through the kernel between cells).

B) “I have a file written by prepare()”#

from cellucid import show_anndata

viewer = show_anndata("my_study.h5ad")  # it names itself (uns['cellucid'])

Practical path (step-by-step + common workflows)#

1) Notebook display vs browser tab#

Cellucid always runs in a browser context (the viewer UI), but you choose where it appears:

2) Opening what you have, or a prepared file#

Open the object or file you have when:

  • you’re iterating quickly in analysis notebooks,

  • you want the viewer to read the .h5ad or Zarr store you already have, in place.

Open a file written by prepare() when:

  • you want reproducibility (the file is a concrete, self-naming artifact),

  • you want speed (a gene-major matrix: every gene is one small read),

  • you want to share the dataset or host it.

3) A minimal “hooks” example (viewer → Python)#

from cellucid import show_anndata

viewer = show_anndata(
    adata,
    dataset_name="My study",
    dataset_id="my-study-v1",
)

@viewer.on_selection
def on_selection(event):
    # event["cells"] is a list[int] of selected indices (row indices into adata)
    print("Selected", len(event.get("cells", [])), "cells")

4) A minimal “control” example (Python → viewer)#

Every action of the viewer is a method; each returns an ActionRequest whose result() (in a later cell) is the viewer’s answer:

# Highlight a few cells (rows of obs, a boolean mask, or obs names)
viewer.highlight_cells(cells=[0, 10, 20], color="#ff0000")

# Color points by an obs field (must exist in the dataset/AnnData)
viewer.color_by(field={"source": "obs", "key": "cell_type"})

# Hide a subset (or pass None for every cell)
viewer.cells_visibility_set(cells=[1, 2, 3], visible=False)

# Reset the camera of the live view
viewer.camera_reset(view="live")

# Read: the fields the viewer has (request.result() in a later cell)
request = viewer.fields_list()

5) Running code once the viewer is ready (robust notebooks)#

The viewer loads after the cell that showed it finishes, so register a hook:

@viewer.on_ready
def ready(event):
    print("Viewer ready:", event)

or read viewer.state.ready in a later cell. (viewer.wait_for_ready() returns at once when it is ready, and raises in a cell that would have to wait: the viewer is heard only between cells.)

6) Capturing a .cellucid-session without downloading (advanced)#

This is the “no browser download” workflow, in two cells: ask, then collect.

request = viewer.request_session()
session = request.result()
print(session.tracker().steps[["id", "label", "origin"]])
plan = session.apply(adata)       # what applying the step shown would change

See Sessions (.cellucid-session files) for what a session contains and how to apply it back onto an AnnData. viewer.load_session(path) goes the other way: it adds a saved session’s trackers to the viewer’s history.


Deep path (how notebook embedding works)#

What happens when you call show_anndata(...)#

  1. The dataset and the launch choices are checked; an in-memory AnnData is planned as a store in priority order, made as the viewer reads it (a MuData as a MuData store), its expression matrices laid out gene-major in the background. Nothing binds a port.

  2. A Widget is shown. Its output frames the Cellucid app, which your browser loads from web_source_url (https://www.cellucid.com by default).

  3. The app asks the widget for its credentials and a private channel, reads the dataset through it (the kernel answers each request from the dataset) and opens the viewer channel through it: commands go to the viewer and interaction events come back.

  4. The widget routes those events to your hook callbacks (e.g., @viewer.on_selection), between cells.

Architecture: Architecture: the viewer channel.


API reference#

Functions#

show_anndata#

cellucid.show_anndata(data, height=600, *, latent_key=None, gene_id_column=None, normalize_embeddings=None, centroid_min_points=None, dataset_name=None, dataset_id=None, obs_keys=None, embedding_keys=None, knn_key=None, expression_source=None, vector_field_default=None, modality=None, web_source_url='https://www.cellucid.com', cache_bytes=None)[source]#

Show an AnnData or MuData object, or any file Cellucid reads, in a notebook.

Returns a cellucid.jupyter.Widget and shows it in the cell’s output: in JupyterLab, Jupyter Notebook 7, VS Code, Google Colab and Databricks it is displayed at once; in marimo, make it the cell’s output (the last expression). The widget reaches this kernel through the notebook’s own connection, so no port, proxy or server address is involved. The viewer reads the data itself, without running prepare first: in place with range requests where the format allows it, once from the start for an R object or a MatrixMarket folder. An in-memory AnnData, and an AnnData, MuData, Loom or 10x file not in priority order, are served as a Zarr store with their cells in priority order, made as the viewer reads it, so a subsample reads only its cells; so is a MuData object. An object’s expression matrices are laid out gene-major in the background (read-only meanwhile), so the viewer opens at once; a cell of the notebook run again stops the widget it showed before.

To open a viewer in a browser tab of its own instead (a script, an SSH tunnel), use AnnDataViewer, or serve_anndata / cellucid serve.

Parameters:
  • data (str | Path | anndata.AnnData | mudata.MuData) – AnnData or MuData object, or path to a file or folder Cellucid reads (see “Supported formats”). A leading ~ in a path is expanded to the user’s home directory.

  • height (int) – Height of the viewer in pixels.

  • latent_key (str | None) – obsm array (a latent space such as scVI or PCA) the viewer’s outlier filter measures in, instead of the data’s own uns['cellucid']['defaults']['latentKey']. The viewer reads the outlier quantiles the data stores for it, or computes them on first use.

  • gene_id_column (str | None) – Exact column in var (raw.var with expression_source="raw") whose values name the genes. If None, genes are named by the index.

  • normalize_embeddings (bool | None) – Whether the viewer scales embeddings into its coordinate range.

  • centroid_min_points (int | None) – The fewest cells a category needs to get outlier quantiles (10 when None).

  • dataset_name (str | None) – Exact non-empty human-readable name without surrounding whitespace or control characters. Defaults to uns['cellucid']['name'], else the file’s name, or AnnData (<n> cells) for an object.

  • dataset_id (str | None) – Portable 1-180 byte ASCII identifier beginning with a letter or digit and otherwise using letters, digits, ., _, or -; no trailing . or Windows device name. Defaults to uns['cellucid']['id'], else made from the file’s name without its extension, or from the object’s label (AnnData (N cells)) (said in one line: sessions are kept under it).

  • obs_keys (Sequence[str] | None) – Exact obs columns the viewer shows, in this order. If None, every column is shown.

  • embedding_keys (Mapping[str, EmbeddingKeySpec] | None) – The embedding of a dimension slot, such as {"2d": "X_tsne"} or {"2d": ("X_pca", [2, 0])} with 0-based columns. Slots not named follow the default rule, and a deterministic best guess applies when no slot resolves.

  • knn_key (str | None) – The obsp matrix the viewer draws as the neighbour graph, such as "connectivities"; read only when a view draws it. None follows the viewer’s default rule.

  • expression_source (str | None) – "X", "raw", or "layer:<name>"; None follows the viewer’s default rule (raw when it holds more genes than X or X is scaled, as Scanpy reads genes; else X).

  • vector_field_default (str | None) – The vector field the overlay opens on: <field> of an obsm array <field>_<basis> drawn over X_<basis> (velocity for velocity_umap).

  • modality (str | None) – The modality a multimodal dataset (a MuData) opens on; None opens the default one. Set only for multimodal data.

  • web_source_url (str) – Where the browser loads the Cellucid app the widget shows: https://www.cellucid.com (the default), or the address of a copy you host. The browser, not the kernel, loads it.

  • cache_bytes (int | None) – What the kernel keeps of what it makes for a store served in priority order, in bytes (least recently used first out); None keeps a 64th of the machine’s memory, 0 nothing (as cellucid.serve_anndata()’s).

Return type:

Widget

Returns:

The cellucid.jupyter.Widget, for hooks and commands.

Supported formats:
  • .h5ad, AnnData Zarr (folder or .zip), .h5mu, MuData Zarr, .loom, 10x .h5 files and Cell Ranger outs/ folders: a file prepare() wrote read in place, any other served in priority order as the viewer reads it

  • MatrixMarket folders (a .mtx[.gz] file of any name, such as GEO’s GSM…_matrix.mtx.gz) and R objects (.rds, .rda, .RData, gzipped or not), read once

  • In-memory AnnData and MuData objects, served in priority order as the viewer reads them, their expression matrices laid out gene-major in the background

Example

>>> from cellucid import show_anndata
>>> viewer = show_anndata(
...     adata,
...     dataset_name="Example",
...     dataset_id="example",
... )
>>>
>>> @viewer.on_selection
... def handle(event):
...     subset = adata[event['cells']]
...     sc.pl.violin(subset, 'gene1')

Note

To publish a dataset for others, prepare it (or publish the .h5ad or Zarr store itself) and share a link; see the data loading guide.

Classes (see dedicated page)#


Edge cases (do not skip)#

Data size and performance#

  • A path is never loaded into the kernel’s memory: the viewer reads an .h5ad, a Zarr store, an .h5mu, a .loom or a 10x file in place with HTTP range requests (only what a view needs), and an R object or a MatrixMarket folder once from the start.

  • An in-memory AnnData is served as a Zarr store with its cells in priority order, each chunk made from the object when the viewer asks for it, so a subsample transfers only its cells; a cell-major sparse X is laid out gene-major once, in the background, into memory-mapped files removed when the viewer stops or Python exits. A MuData is written to a temporary Cellucid-ready .h5mu when the viewer starts.

“My embedding is not 2D/3D”#

  • Cellucid supports 1D/2D/3D embeddings; if the embedding is missing or has an unexpected shape, you’ll see missing plot / errors.

Duplicate gene identifiers#

  • The viewer makes repeated gene names unique (MATR3, MATR3-1), so every gene stays reachable; name genes by another var column with gene_id_column when the index holds identifiers you do not search by.


Troubleshooting (symptom → diagnosis → fix)#

Symptom: “Viewer does not appear in the notebook output”#

Likely causes:

  • The notebook cannot show widgets: the output shows text (“This output is a Cellucid viewer (a notebook widget)…”) instead.

  • You are not in a notebook (a plain script): show_anndata() raises and names the browser-tab alternatives.

Fix:

  • Install anywidget where the notebook server runs (pip install anywidget) and reload the page, or use a notebook that shows widgets (Supported environments matrix).

  • Outside a notebook use AnnDataViewer or cellucid serve.


Symptom: “The output says the page has not answered as a Cellucid viewer”#

Likely causes:

  • The browser cannot load the app from web_source_url (offline, a blocked network), or that address is not a Cellucid build that can be embedded.

Fix:

  • Check that https://www.cellucid.com opens in the same browser, or host a copy and pass web_source_url=.


Symptom: “The output says it is waiting for the notebook kernel”#

What’s happening:

  • A cell is still running; the kernel answers the viewer between cells.

Fix:

  • Let the cell finish (or interrupt it); the viewer connects at once.


Symptom: “wait_for_ready() / request.result() raised at once”#

What’s happening:

  • A notebook widget hears the viewer only between cells, so a call that waits for the viewer cannot be answered in the cell that waits; the message says what to do instead.

Fix:

  • Use a hook or viewer.state in a later cell; for an action’s answer or a session, keep the request (request = viewer.request_session()) in one cell and call request.result() in the next.


Symptom: “I registered @viewer.on_selection but nothing prints”#

Likely causes:

  • The viewer is not ready yet (it loads once the cell that showed it ends).

  • You selected nothing (or selection tools differ from expectation).

  • A cell is still running: hooks run when the kernel is idle.

How to confirm:

  • In a new cell, check viewer.state.ready.

Fix:

  • Ensure the viewer is loaded, then select cells (lasso/shift-click/etc.).


Symptom: “viewer.color_by(...) does nothing”#

Likely causes:

  • The field name does not exist in obs (or has a different spelling/case).

How to confirm:

  • Collect the request: request.result() (in a later cell) raises ActionError with the viewer’s reason, which lists fields like the one named.

  • Register @viewer.on_command_error: a refused action reports why.

Fix:

  • Use the exact field name.

  • Prefer a file written by prepare() for stable field naming across sessions.


Symptom: “viewer.highlight_cells(...) does nothing”#

Likely causes:

  • Rows are out of range (wrong dataset / wrong cell order): Python refuses a row beyond the dataset before sending.

How to confirm:

  • Try highlighting a small set like cells=[0].

  • Collect the request (request.result()) or register @viewer.on_command_error: a refused action reports why.

Fix:

  • Ensure indices correspond to the dataset loaded in the viewer.

  • If you subset/reorder AnnData, re-run the viewer from the same object.


Symptom: “Selections map to the wrong cells in my AnnData”#

Likely causes:

  • The viewer is connected to a dataset whose row order does not match your adata.

How to confirm:

  • Select a single cell and print identifying metadata from adata.obs.iloc[cell_index].

  • Compare with what you expect in the UI.

Fix:

  • Ensure you apply selection indices to the same AnnData used to create/serve the viewer.

  • Avoid reordering adata after starting the viewer; restart the viewer after reindexing.


Symptom: “My hook callback crashes silently”#

What’s happening:

  • Hooks run on the viewer channel’s thread, not in your cell. An exception a hook raises is logged with its traceback (logger cellucid.viewer_channel, “A viewer event hook failed”), never raised in the notebook. The event is still recorded in viewer.state and handed to wait_for_event (it is recorded before any hook runs), and the other hooks still run for it. The channel stays open.

How to confirm:

  • Enable debug logging:

    import logging
    logging.basicConfig(level=logging.DEBUG)
    

Fix:

  • Add defensive checks inside callbacks (validate keys, lengths, types).

  • Start with @viewer.on_message to inspect raw event payloads.


Symptom: “I don’t know what’s wrong / I need a diagnostic dump”#

Fix:

  • Run:

    report = viewer.debug_connection()
    report
    
  • For a notebook widget the report includes where the app is loaded from, the connected outputs, whether the channel is open, the readiness state and recent events (it waits for nothing). For a browser-tab AnnDataViewer it also probes the server (health, info, every listed store), the web build cache, and pings the viewer over the channel.


See also#