Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ Render H&E tissue, overlay spots, color by gene expression and by cluster,
and finish with a publication-style figure.
:::

:::{grid-item-card} Interactive region annotation
:link: interactive_annotate
:link-type: doc
:img-top: interactive_annotate.png

Draw regions of interest directly on a `spatialdata-plot` canvas with
`sdata.pl.annotate(...)` and persist them as a `ShapesModel` element.
:::

::::

```{toctree}
:hidden:
:maxdepth: 1

visium_mouse_brain
interactive_annotate
```
260 changes: 260 additions & 0 deletions examples/interactive_annotate.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "anno-intro",
"metadata": {},
"source": [
"# Interactive region annotation\n",
"\n",
"This tutorial shows how to use `sdata.pl.annotate()` to draw regions of interest directly on a `spatialdata-plot` canvas inside a notebook and persist them as a `ShapesModel` element. The widget is a custom [anywidget] that draws client-side, so it works over SSH (Jupyter or VSCode-Remote-SSH) without streaming PNG frames per mouse-move.\n",
"\n",
"**Dataset**: a Visium H&E mouse brain section, downloaded by `squidpy.datasets.visium_hne_sdata` from the scverse example data host. The download (~400 MB) is cached after the first run.\n",
"\n",
"**Requires the `interactive` extra:**\n",
"\n",
"```bash\n",
"pip install 'spatialdata-plot[interactive]'\n",
"```\n",
"\n",
"[anywidget]: https://anywidget.dev"
]
},
{
"cell_type": "markdown",
"id": "anno-load-hdr",
"metadata": {},
"source": [
"## Loading the dataset\n",
"\n",
"`squidpy.datasets.visium_hne_sdata()` returns a `SpatialData` object with the multi-resolution H&E image (`'hne'`) and the spot polygons (`'spots'`), both aligned in the `'global'` coordinate system."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-load",
"metadata": {},
"outputs": [],
"source": [
"import squidpy as sq\n",
"from shapely.geometry import Polygon\n",
"\n",
"import spatialdata as sd\n",
"import spatialdata_plot # noqa: F401 (registers the .pl accessor)\n",
"from spatialdata.models import ShapesModel\n",
"from spatialdata.transformations.transformations import Identity\n",
"\n",
"sdata = sq.datasets.visium_hne_sdata()\n",
"sdata"
]
},
{
"cell_type": "markdown",
"id": "anno-inspect-hdr",
"metadata": {},
"source": [
"## Inspect what we'll annotate\n",
"\n",
"Before drawing, let's render the image so we know what to outline."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-inspect",
"metadata": {},
"outputs": [],
"source": [
"sdata.pl.render_images(\"hne\").pl.show()"
]
},
{
"cell_type": "markdown",
"id": "anno-launch-hdr",
"metadata": {},
"source": [
"## Launching the widget\n",
"\n",
"Call `sdata.pl.annotate(coordinate_system, element)` to open the drawing canvas. The image is rendered once via the standard `render_images` pipeline, exported to PNG, and laid under a client-side SVG drawing surface; all interaction happens in the browser, and only the final shape geometry round-trips to the kernel on Save.\n",
"\n",
"```python\n",
"sdata.pl.annotate(\n",
" coordinate_system=\"global\",\n",
" element=\"hne\",\n",
" max_width=880, # display hint; underlying render is 840×840\n",
" persist=True, # shows a \"Write to disk\" button\n",
")\n",
"```\n",
"\n",
"> The code above is intentionally a Markdown block, not a code cell — the widget needs a live JS runtime, which the static docs build can't provide. Run the line in your own notebook (Jupyter Lab or VSCode-Remote-SSH) to see the canvas.\n",
"\n",
"**Drawing tools**: rectangle (drag), polygon (click vertices, snap-to-first or Enter to close), lasso (drag freehand). \n",
"**Shortcuts**: `R` / `P` / `L` switch tool · wheel zoom · Shift+drag pan · Alt+click a shape to delete · Ctrl+Z undo · `F` fit · `Esc` cancel in-progress shape.\n",
"\n",
"When you click **Save** the shapes on the canvas are committed to `sdata.shapes[<name>]` as a single `ShapesModel` (multiple rows if you drew multiple shapes). The optional **Write to disk** button calls `sdata.write_element(<name>)` to persist to the backing zarr."
]
},
{
"cell_type": "markdown",
"id": "anno-gif",
"metadata": {},
"source": [
"![Drawing a region with sdata.pl.annotate](interactive_annotate.gif)\n",
"\n",
"*Live recording of the widget. Replace this file with your own capture before publishing the tutorial.*"
]
},
{
"cell_type": "markdown",
"id": "anno-simulate-hdr",
"metadata": {},
"source": [
"## What the widget produces\n",
"\n",
"To keep the rest of this notebook reproducible without a live kernel, the next cell creates the same `ShapesModel` the widget would have written if you'd drawn a polygon around the hippocampus and clicked Save with the name `'tumor_region'`. After this cell, downstream code is identical regardless of whether you ran the widget or this simulated commit."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-simulate",
"metadata": {},
"outputs": [],
"source": [
"# Pretend the user drew this polygon in the widget and clicked Save with\n",
"# name=\"tumor_region\". Coordinates are in the 'global' coordinate system\n",
"# of the visium_hne_sdata dataset.\n",
"hippocampus_polygon = Polygon(\n",
" [\n",
" (3200, 4800),\n",
" (4800, 4400),\n",
" (5600, 5200),\n",
" (5400, 6400),\n",
" (4200, 6600),\n",
" (3200, 6000),\n",
" ]\n",
")\n",
"\n",
"import geopandas as gpd\n",
"\n",
"sdata.shapes[\"tumor_region\"] = ShapesModel.parse(\n",
" gpd.GeoDataFrame({\"geometry\": [hippocampus_polygon]}),\n",
" transformations={\"global\": Identity()},\n",
")\n",
"sdata.shapes[\"tumor_region\"]"
]
},
{
"cell_type": "markdown",
"id": "anno-overlay-hdr",
"metadata": {},
"source": [
"## Working with the saved region\n",
"\n",
"The committed element is a normal `ShapesModel` — every downstream API in `spatialdata` and `spatialdata-plot` treats it like any other shapes layer. Here we overlay it on the H&E to confirm placement."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-overlay",
"metadata": {},
"outputs": [],
"source": [
"(\n",
" sdata\n",
" .pl.render_images(\"hne\")\n",
" .pl.render_shapes(\"tumor_region\", outline_color=\"#22d3ee\", fill_alpha=0.2)\n",
" .pl.show()\n",
")"
]
},
{
"cell_type": "markdown",
"id": "anno-query-hdr",
"metadata": {},
"source": [
"## Cropping the dataset to the region\n",
"\n",
"Because the region is a registered `ShapesModel`, `sdata.query.polygon` can subset every element down to what falls inside it. Useful for focused analyses or quick QC on a single anatomical structure."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-query",
"metadata": {},
"outputs": [],
"source": [
"subset = sd.polygon_query(\n",
" sdata,\n",
" sdata[\"tumor_region\"],\n",
" target_coordinate_system=\"global\",\n",
")\n",
"subset"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-query-plot",
"metadata": {},
"outputs": [],
"source": [
"(\n",
" subset\n",
" .pl.render_images(\"hne\")\n",
" .pl.render_shapes(\"spots\", fill_alpha=0.4)\n",
" .pl.show()\n",
")"
]
},
{
"cell_type": "markdown",
"id": "anno-repro-hdr",
"metadata": {},
"source": [
"## For reproducibility"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "anno-repro",
"metadata": {},
"outputs": [],
"source": [
"# ruff: noqa: F401, F811, I001, E402\n",
"# fmt: off\n",
"import spatialdata_plot\n",
"\n",
"%load_ext watermark\n",
"# fmt: on\n",
"\n",
"%watermark -v -m -p spatialdata,spatialdata_plot,squidpy,anywidget,matplotlib,numpy"
]
}
],
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading