Skip to content

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

Draft
timtreis wants to merge 2 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 2 commits into
scverse:mainfrom
timtreis:delegate-plots-to-sdata-plot

Conversation

@timtreis
Copy link
Copy Markdown
Member

@timtreis timtreis commented May 12, 2026

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

Test plan

  • Local self-tests: 38 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 (groups, crop_coord, outline, size per-library, palette variants, fig/ax passthrough, edges, scalebar)
  • A/B run under SQUIDPY_USE_SDATAPLOT=1 against existing tests/plotting/test_spatial_static.py: 16 pass cleanly, 16 fail (one is fixture pollution in the session-scoped fixture; the rest are expected pixel-diff against legacy baselines because the renderer is now spatialdata-plot)
  • CI under SQUIDPY_USE_SDATAPLOT=1 job (follow-up — not in this PR)
  • Reference-image baselines refreshed once we commit to flipping the default (deliberately deferred so users on main keep matching the legacy renderer)

Public API

Unchanged. Existing sq.pl.spatial_scatter / sq.pl.spatial_segment signatures and behavior are identical when the flag is off. The internal entrypoints are underscore-prefixed (_spatial_scatter_via_sdata_plot, _spatial_segment_via_sdata_plot).

timtreis and others added 2 commits May 12, 2026 13:48
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>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 75.93458% with 103 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.69%. Comparing base (373228d) to head (6205e13).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/squidpy/pl/_sdata_delegation/_capture.py 72.72% 29 Missing and 13 partials ⚠️
src/squidpy/pl/_sdata_delegation/_render.py 60.00% 22 Missing and 12 partials ⚠️
src/squidpy/pl/_sdata_delegation/_adapter.py 79.00% 17 Missing and 4 partials ⚠️
src/squidpy/pl/_spatial.py 33.33% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1173      +/-   ##
==========================================
+ Coverage   73.56%   73.69%   +0.13%     
==========================================
  Files          44       48       +4     
  Lines        6929     7357     +428     
  Branches     1174     1239      +65     
==========================================
+ Hits         5097     5422     +325     
- Misses       1347     1419      +72     
- Partials      485      516      +31     
Files with missing lines Coverage Δ
src/squidpy/pl/_sdata_delegation/_intent.py 100.00% <100.00%> (ø)
src/squidpy/pl/_spatial.py 87.09% <33.33%> (-5.77%) ⬇️
src/squidpy/pl/_sdata_delegation/_adapter.py 79.00% <79.00%> (ø)
src/squidpy/pl/_sdata_delegation/_render.py 60.00% <60.00%> (ø)
src/squidpy/pl/_sdata_delegation/_capture.py 72.72% <72.72%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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