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 round-trip the current Cellucid session back into AnnData.

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

Viewer startup reads the exact web build from Cellucid’s configured public source. The AnnData itself remains 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 used when a session is applied back to AnnData. Keep it unchanged for the same logical dataset.

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
ready = viewer.wait_for_ready(timeout=120)
viewer.set_color_by("clusters")
viewer.highlight_cells([0, 1, 2], color="#E76F51")

print("Viewer ready:", ready)
viewer.state

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.")

Round-trip the current session into AnnData#

After creating a highlight group or a user-defined field in the viewer, this cell captures the session and applies it to a new in-memory AnnData object. The stable dataset identity is checked before any mutation.

bundle = viewer.get_session_bundle(timeout=180)
in_memory = ad.read_h5ad(DATA_PATH)

updated, summary = bundle.apply_to_anndata(
    in_memory,
    expected_dataset_id=DATASET_ID,
    inplace=False,
    return_summary=True,
)

print(summary)
print("New obs columns:", sorted(set(updated.obs.columns) - set(in_memory.obs.columns)))
connection = viewer.debug_connection(timeout=15)
identity = connection["dataset_identity_probes"][DATASET_ID]["identity"]

print("Server health:", connection["server_health"]["status"])
print("Dataset identity:", identity["id"])
print("Cells served:", identity["stats"]["n_cells"])
print("Frontend round-trip:", connection["frontend_roundtrip"]["ok"])
print("Exact web build:", connection["viewer_index_probe"]["build_id"])
bundle.path.unlink(missing_ok=True)
viewer.stop()
adata.file.close()
print("Session artifact removed and local viewer server stopped.")