Skip to content

Add spatialdata-plot delegation backend (gated by feature flag) - #1173

Draft
timtreis wants to merge 9 commits into
scverse:mainfrom
timtreis:delegate-plots-to-sdata-plot
Draft

Add spatialdata-plot delegation backend (gated by feature flag)#1173
timtreis wants to merge 9 commits into
scverse:mainfrom
timtreis:delegate-plots-to-sdata-plot

Conversation

@timtreis

@timtreis timtreis commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Introduces a parallel rendering pipeline for sq.pl.spatial_scatter and sq.pl.spatial_segment that delegates to spatialdata-plot under SQUIDPY_USE_SDATAPLOT=1. Off by default — the legacy implementation is untouched. Lays the foundation for closing the spatial-plotting half of #912.

Architecture

capture-intent -> adapter -> spatialdata-plot in src/squidpy/pl/_sdata_delegation/:

Module Role
_intent.py Frozen dataclasses (Intent, DataIntent, RenderIntent, LayoutIntent, PostRenderIntent, PanelIntent)
_capture.py Parses squidpy kwargs into an Intent; panel expansion + per-library scalar resolution happen here
_adapter.py Builds a transient SpatialData from a Visium-style AnnData (one coordinate system per library; shapes/points/labels + optional image + shared table). pairwise=True preserves obsp for render_graph
_render.py Per-panel render_images -> render_graph -> render_shapes/labels/points -> show, with post-render hooks for title, frameon, crop_coord

The shim is short-lived by design — _make_tmp_sdata only handles the AnnData -> SpatialData translation and disappears when AnnData input is removed at v2.0 (see plans/delegate-plots-to-sdata-plot.md).

Kwarg coverage

Captured and forwarded:

  • shape (circle/hex/square/visium_hex/None for points)
  • color (single feature or N-feature panel grid with library_first ordering)
  • groups, palette (dict / list / Colormap), cmap, norm + vmin/vmax/vcenter folded into Normalize
  • alpha, na_color, outline (triple-render passes)
  • connectivity_key + edges_* via render_graph
  • size (scalar or per-library sequence -> render_shapes(scale=))
  • crop_coord, scalebar_dx/scalebar_units (version-gated), title, axis_label, frameon, fig/ax
  • img_alpha, img_cmap, img_channel, layer/use_raw/alt_var

legend_loc='on data' emits a DeprecationWarning and falls back to the default (per maintainer call: known buggy in spatial coords, slated for removal).

Dependency bump

spatialdata-plot>=0.3.4 for render_graph (PR scverse/spatialdata-plot#592) and scalebar_dx/scalebar_units on show() (PR scverse/spatialdata-plot#648).

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.36185% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.62%. Comparing base (e276aa4) to head (6a5fd19).

Files with missing lines Patch % Lines
src/squidpy/pl/_sdata_delegation/_render.py 61.66% 26 Missing and 20 partials ⚠️
src/squidpy/pl/_sdata_delegation/_capture.py 80.12% 23 Missing and 10 partials ⚠️
src/squidpy/pl/_sdata_delegation/_source.py 68.35% 15 Missing and 10 partials ⚠️
src/squidpy/pl/_sdata_delegation/_adapter.py 80.39% 16 Missing and 4 partials ⚠️
src/squidpy/pl/_spatial.py 66.66% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1173      +/-   ##
==========================================
- Coverage   78.62%   78.62%   -0.01%     
==========================================
  Files          63       68       +5     
  Lines        9330     9891     +561     
  Branches     1557     1654      +97     
==========================================
+ Hits         7336     7777     +441     
- Misses       1422     1494      +72     
- Partials      572      620      +48     
Files with missing lines Coverage Δ
src/squidpy/pl/_sdata_delegation/_intent.py 100.00% <100.00%> (ø)
src/squidpy/pl/_spatial.py 90.32% <66.66%> (-2.54%) ⬇️
src/squidpy/pl/_sdata_delegation/_adapter.py 80.39% <80.39%> (ø)
src/squidpy/pl/_sdata_delegation/_source.py 68.35% <68.35%> (ø)
src/squidpy/pl/_sdata_delegation/_capture.py 80.12% <80.12%> (ø)
src/squidpy/pl/_sdata_delegation/_render.py 61.66% <61.66%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timtreis timtreis self-assigned this Jun 23, 2026
timtreis and others added 3 commits August 12, 2026 15:05
Introduces a parallel rendering pipeline for sq.pl.spatial_scatter and
sq.pl.spatial_segment that routes through spatialdata-plot under
SQUIDPY_USE_SDATAPLOT=1. Off by default. Closes the spatial-plotting half
of scverse#912 in shim form so the legacy and new paths can run side-by-side
during the migration window.

Pipeline (capture-intent -> adapter -> spatialdata-plot):
- _capture: parses squidpy kwargs into a structured Intent
  (DataIntent / RenderIntent / LayoutIntent / PostRenderIntent / PanelIntent).
  Panel expansion at capture, per-library values resolved to PanelIntent
  scalars. Folds vmin/vmax/vcenter into a Normalize, routes Colormap and
  list palettes through cmap, infers groups from dict palettes.
- _adapter: builds a transient SpatialData from Visium-style AnnData
  (one coordinate system per library; shapes/points/labels element +
  optional image + shared table). pairwise=True on concat to preserve
  obsp for render_graph.
- _render: per-panel render_images -> render_graph -> render_shapes /
  render_labels / render_points -> show, with post-render hooks for
  title/frameon/crop_coord.

Surface covered: shape (circle/hex/square/visium_hex/None for points),
color (single or multi-feature with library_first ordering), groups,
palette (dict/list/Colormap), cmap, norm + vmin/vmax/vcenter, alpha,
na_color, outline (triple-render), connectivity_key + edges_*,
size (scalar or per-library), crop_coord, scalebar_dx/units (when
spatialdata-plot exposes them), title, axis_label, frameon, fig/ax,
img_alpha/img_cmap/img_channel, layer/use_raw/alt_var. legend_loc='on data'
emits a DeprecationWarning and falls back to the default.

Bump: spatialdata-plot>=0.3.4 for render_graph and scalebar in show().

Tests: 38 new tests in test_spatial_scatter_sdataplot.py covering the
three identified user happy paths (Visium+H&E categorical, Visium+H&E
continuous N-gene grid, segmentation-mask cell-type coloring) plus
stress-test parity.

Public API unchanged. Legacy implementation untouched. Plan in
plans/delegate-plots-to-sdata-plot.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Maintainer-review pass on the delegation backend:

M1 Image materialization: replace np.asarray+transpose with np.moveaxis so
   dask-backed images stay lazy until render. Critical for Visium HD scale.

M2/S3 Table model: build one TableModel per library instead of a single
   concat(pairwise=True). Avoids materializing a cross-library obsp at
   O(N_total^2). render_* calls pass table_name=f'{lib}_table'.

S1 Outline: use sdata-plot v0.3.4 tuple outline_color/outline_width support
   to draw both rings in one render_shapes call (was three).

S2 Thread spatial_key through DataIntent.coordinate_system to the adapter
   so a non-default obsm key is honored.

S4 Skip legacy reference-image tests under SQUIDPY_USE_SDATAPLOT=1
   (tests/plotting/conftest.py). Avoids confusing pixel-diff failures
   for users kicking the tires.

S5 Function-scope MIBI-TOF fixture with explicit copy so adapter-side
   obs mutations don't leak across TestPath3Segmentation tests.

S6 Single mpl-recognized color string passed as palette routes to the
   panel.color slot rather than failing sdata-plot's palette+groups
   validation.

Quality: collapse needs_shapes/needs_labels/needs_points booleans into a
single DataIntent.element_kind Literal. Extract _apply_color_override.
Drop unused Intent fields (shapes_layer/labels_layer/image_layer/points_layer,
scalebar_kwargs, scale_factor). Drop dead _SHOW_SUPPORTS_SCALEBAR runtime
guard. Drop the capture_scatter_intent_path1 alias.

Adapter now uses Key.uns.spot_diameter instead of raw scalefactor lookups.

All 38 self-tests still passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Legend/colorbar/save/axis_label/scalebar params and image styling were
captured into the Intent but never threaded or forwarded, so they were
silently ignored. Thread them through capture and forward into the
per-panel show()/render_images/render_graph calls:

- _show_kwargs() forwards legend_loc/fontsize/fontweight/fontoutline,
  na_in_legend, colorbar, scalebar_dx/units/params into show().
- render_images(alpha=, cmap=, channel=) for img_alpha/img_cmap/img_channel.
- save -> save_fig(fig, path=save) once at the end (not per-panel).
- edges_kwargs -> render_graph (edge_alpha/linestyle/weight_key); unknown
  keys raise instead of being dropped.
- axis_label -> post-render set_xlabel/ylabel (no native show() kwarg yet,
  scverse/spatialdata-plot#763).
- wspace/hspace added to both capture signatures (previously rejected) and
  applied via subplots_adjust when the grid is backend-owned.

TestWiredKwargs asserts observable effects (colorbar/legend present-absent,
axis labels, save writes a file, edges reject-list) rather than internals.
@timtreis
timtreis force-pushed the delegate-plots-to-sdata-plot branch from c792632 to a1aa420 Compare August 12, 2026 13:56
Render a user's SpatialData directly instead of only AnnData via the shim.

- _Source abstraction (_source.py): capture's only input coupling is library
  resolution + element naming, so _AnnDataSource (shim names) and
  _SpatialDataSource (names resolved from coordinate systems + tables) share
  one capture path.
- PanelIntent carries resolved element/image/table/graph names; _render no
  longer derives names from library_id, so it is input-agnostic.
- New public kwargs shapes_layer/points_layer/image_layer/table (scatter) and
  labels_layer/image_layer/table (segment) disambiguate when a coordinate
  system holds multiple candidate elements; ambiguity raises listing them.
- Entrypoints branch on SpatialData (skip the transient-sdata shim). AnnData
  input now emits a DeprecationWarning (removal target v2.0). use_raw and
  library_key raise on SpatialData input (AnnData-only concepts).

TestSpatialDataNativeInput covers categorical/continuous render, use_raw and
library_key rejection, element ambiguity + shapes_layer disambiguation, and the
AnnData deprecation warning.
…kend

- scale_factor: previously rejected via **unsupported, now an explicit image
  scale-factor override for the AnnData shim (threaded intent -> _make_tmp_sdata;
  V1 confirmed the 1/scalef Scale direction keeps spots aligned). Ignored on
  SpatialData input, which already carries its own transforms.
- Clarify the shim's uns[spatial] KeyError to point at SpatialData input for
  non-Visium layouts (no silent data drop).
- Tests: scale_factor accepted+stored; render parametrized over
  [anndata, spatialdata]; public-API path (SQUIDPY_USE_SDATAPLOT=1 through
  sq.pl.spatial_scatter) returns a Figure and warns on AnnData input.
- CI: a required test-sdataplot-backend job runs the delegation suite with the
  flag on (legacy reference suite is skipped under the flag by conftest).
- Docs: release note (experimental opt-in backend + AnnData deprecation);
  corrected the stale capture docstrings (connectivity_key/spatial_key are
  supported, legend_loc='on data' warns) and documented the *_layer/table kwargs.
An informal legacy-vs-delegation render comparison surfaced a silent
value-source difference: for a continuous gene the legacy colorbar ran 0-3.0
(raw counts) but the delegation backend ran 0-1.6 (.X). Legacy/scanpy resolve
use_raw=None to True when no layer is set and adata.raw exists; _resolve_use_raw
treated None as "use .X", so flipping the flag silently changed plotted values.

Resolve use_raw=None the same way (layer is None and adata.raw is not None) and
thread the layer through. Regression test asserts the default color mapping uses
raw (larger vmax) while use_raw=False uses .X.
…names)

Quality cleanup from a reuse/simplify/efficiency/altitude review; no behavior change.

- __init__: fold the two near-identical entrypoints into one _delegate(input_obj,
  capture_fn, **kwargs); the input-dispatch policy now lives in one place.
- _capture: collapse a dead _resolve_palette branch (both arms returned the same
  value); extract the duplicated legend_loc='on data' deprecation into
  _downgrade_on_data_legend().
- _assign_names: resolve element/image/table names once per unique library instead
  of once per (library x color) panel - avoids re-running
  filter_by_coordinate_system for every color on SpatialData input.
- _intent/_render: drop PanelIntent.graph_element_name (it always equalled
  element_name); the graph render path reads element_name directly.
- _source: drop the _ELEMENT_CONTAINER identity map; ElementKind values already are
  the container attribute names.
Sphinx warns on the duplicate release/notes-dev.{md,rst} pair and treats
warnings as errors, failing the Read the Docs build.
The v0.8.0 template sync (scverse#1257) deleted docs/extensions/typed_returns.py,
which renders numpy-style Returns type annotations as clean cross-references.
Without it, public functions annotated '-> NDArray' emit an unresolvable
numpy._typing._array_like.NDArray reference, and the docs build (run with -W)
fails. This is why main's Read the Docs build is currently red.

Restore the extension and re-register it in conf.py. Verified green with a
full 'sphinx-build -M html docs docs/_build -W'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant