Explore real Pancreas data in Jupyter#

This notebook is a small, complete Cellucid workflow using the 2,531-cell pancreatic endocrinogenesis AnnData file checked into this repository. It does not download a dataset and it does not depend on a private filesystem.

You will load the AnnData, open the embedded viewer, color by the real clusters annotation, receive a selection in Python, and bring what you made in the viewer back into AnnData: in one call with viewer.pull(), and as a saved session.

From the repository root, install and launch JupyterLab with:

python -m pip install -e . jupyterlab
jupyter lab docs/user_guide/python_package/f_notebooks_tutorials/jupyter_pancreas.ipynb

The viewer is a notebook widget: your browser loads the Cellucid app from www.cellucid.com and reads the AnnData from this kernel through the notebook’s own connection. The data stays on the machine running this notebook.

from pathlib import Path

import anndata as ad

from cellucid import show_anndata

DATASET_RELATIVE_PATH = Path(
    "docs/user_guide/python_package/f_notebooks_tutorials/"
    "datasets/endocrinogenesis_day15.5_raw.h5ad"
)


def find_pancreas_h5ad(start: Path) -> Path:
    """Find the checked-in tutorial input from any directory inside the repo."""
    for base in (start.resolve(), *start.resolve().parents):
        candidate = base / DATASET_RELATIVE_PATH
        if candidate.is_file():
            return candidate
        candidate = base / "datasets" / DATASET_RELATIVE_PATH.name
        if candidate.is_file():
            return candidate
    raise FileNotFoundError(
        f"Could not find the checked-in Pancreas tutorial file {DATASET_RELATIVE_PATH}. "
        "Run this notebook from a clone of the cellucid-python repository."
    )


DATA_PATH = find_pancreas_h5ad(Path.cwd())
DATA_PATH
adata = ad.read_h5ad(DATA_PATH, backed="r")

assert adata.n_obs == 2531
assert "X_umap_2d" in adata.obsm
assert "clusters" in adata.obs

print(adata)
print("Cluster labels:", list(adata.obs["clusters"].cat.categories))
print("Available layers:", list(adata.layers.keys()))

Open the embedded viewer#

dataset_id is a stable identity: every session saved on this viewer names it, and applying one to an AnnData whose uns['cellucid']['id'] names another dataset is refused. Keep it unchanged for the same logical dataset. The viewer appears when this cell finishes: the notebook hands the widget’s messages to the kernel between cells.

DATASET_ID = "pancreas-endocrinogenesis-day15-5"

viewer = show_anndata(
    DATA_PATH,
    height=650,
    dataset_name="Pancreatic endocrinogenesis (day 15.5)",
    dataset_id=DATASET_ID,
)
viewer

Commands are sent at once; one sent before the viewer has loaded waits and is delivered when it connects.

viewer.color_by(field={"source": "obs", "key": "clusters"})
viewer.highlight_cells(cells=[0, 1, 2], color="#E76F51")

print("Viewer ready:", viewer.state.ready)  # None until the viewer has opened the dataset

Send a selection from the viewer to Python#

Run the next cell, then make and confirm a selection in the embedded viewer. Each confirmed selection prints its cluster composition without copying the full expression matrix.

@viewer.on_selection
def summarize_selection(event):
    cells = event.get("cells") or []
    if not cells:
        print("Selection is empty")
        return
    counts = adata.obs.iloc[cells]["clusters"].value_counts()
    print(f"Selected {len(cells)} cells")
    print(counts)


print("Selection hook registered. Confirm a selection in the viewer.")

Pull what you made back into AnnData#

Create a highlight group or a user-defined field in the viewer, then run the next cell: viewer.pull() writes every highlight group, label field and score the viewer holds onto obs of an in-memory AnnData, at once.

in_memory = ad.read_h5ad(DATA_PATH)
summary = viewer.pull(in_memory)

print("New obs columns:", list(summary.columns))

Round-trip the current session into AnnData#

A saved session holds the viewer’s whole history: the same results, the view itself, and every step that made them. Ask the viewer for it in one cell and collect it in the next: the viewer answers between cells. Applying it plans first: the AnnData is checked against the dataset the session was saved on (its cells, their order, the data mapping), the plan lists every change one row each, and plan.apply() writes them.

request = viewer.request_session()
session = request.result()

updated = ad.read_h5ad(DATA_PATH)
plan = session.apply(updated)  # what applying the step the viewer shows would change
print(plan.table[["kind", "name", "status", "note"]])

report = plan.apply()  # writes the rows into `updated` and records the apply
print("Columns written:", [] if report.provenance is None else list(report.provenance.columns))
connection = viewer.debug_connection()

print("Transport:", connection["transport"])
print("App loaded from:", connection["web_source_url"])
print("Connected outputs:", connection["connected_outputs"])
print("Channel connected:", connection["channel_connected"])
print("Dataset:", connection["dataset"])
print("Viewer ready:", connection["state"]["ready"])
session.path.unlink(missing_ok=True)
viewer.stop()
adata.file.close()
print("Session artifact removed and viewer stopped.")