Skip to content

Add scanpy-style multi-panel color=[...] for render_shapes and render_labels#686

Open
timtreis wants to merge 6 commits into
mainfrom
feat/issue-611-multipanel-color
Open

Add scanpy-style multi-panel color=[...] for render_shapes and render_labels#686
timtreis wants to merge 6 commits into
mainfrom
feat/issue-611-multipanel-color

Conversation

@timtreis
Copy link
Copy Markdown
Member

@timtreis timtreis commented May 28, 2026

Summary

Closes #611. render_shapes and render_labels now accept a list of column/key names for color, producing one panel per key — the scanpy color=[...] convention.

sdata.pl.render_shapes("blobs_circles", color=["gene1", "gene2", "gene3"]).pl.show(ncols=3)
  • A string list is expanded at render time into one set of render params per key, reusing the existing single-color validation unchanged; each entry is tagged with a new panel_key.
  • show() lays out one panel per key; scalar-colored render calls (e.g. a background image) are drawn into every panel as a shared layer.
  • show(ncols=...) controls the grid width; each panel is titled by its key.

A list[str] is statically distinct from ColorLike's list[float], so the type surface stays clean (color: ColorLike | list[str] | None).

Behavior & constraints

  • Additive / backward compatible: scalar, RGBA, and column color calls are unchanged. A length-1 list normalizes to a scalar color.
  • Multi-panel color requires a single coordinate system (clear error otherwise, naming the systems).
  • Only one render_* call per figure may pass a list (others must be scalar).
  • Duplicate keys are rejected; invalid keys are aggregated into one error before any drawing.
  • groups / palette / cmap / norm are broadcast to every panel; each continuous panel auto-scales independently; colorbars/legends are per-panel.

Drive-by fix

_copy() now shallow-copies plotting_tree, so appending a render step to one chain no longer mutates a sibling chain branched off the same object (this previously also let branched chains bleed layers into each other).

timtreis added 3 commits May 28, 2026 20:46
…_labels

`render_shapes` and `render_labels` now accept a list of column/key names for
`color`, producing one panel per key (like scanpy's `color=[...]`). A string
list is expanded at render time into one set of render params per key, each
tagged with a new `panel_key`; `show()` lays out one panel per key and draws
scalar-colored render calls into every panel as a shared background.
`show(ncols=...)` controls the grid width.

Constraints: multi-panel color requires a single coordinate system, only one
`render_*` call per figure may pass a list, duplicate keys are rejected, and
invalid keys are reported together before drawing.

Also shallow-copy `plotting_tree` in `_copy()` so appending a render step to one
chain no longer mutates a sibling chain branched off the same object.

Closes #611
…avioral tests

Add `test_plot_*` image-comparison tests covering the scanpy-style `color=[...]`
feature: multi-panel shapes (continuous + categorical), shared image background
with `ncols` wrapping, and multi-panel labels. Baselines are generated on CI.

Consolidate the multi-panel behavioral tests into a tight set: one structural
test (panel count + titles + grid geometry), one parametrized scalar-forms test
(length-1 list and RGB float list stay single-panel), one parametrized
invalid-input test (empty/duplicate/mixed/bad-key/two-lists), and the
branch-independence regression.
Generated from the CI visual_test_results artifact (py3.11-stable). Covers
multi-panel shapes (continuous + categorical), shared image background with
ncols wrapping, and multi-panel labels.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 28, 2026

Codecov Report

❌ Patch coverage is 95.89041% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.25%. Comparing base (c499e8c) to head (00c4b67).

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/basic.py 91.66% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #686      +/-   ##
==========================================
+ Coverage   75.98%   76.25%   +0.26%     
==========================================
  Files          14       14              
  Lines        4156     4211      +55     
  Branches      964      982      +18     
==========================================
+ Hits         3158     3211      +53     
- Misses        647      648       +1     
- Partials      351      352       +1     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/render_params.py 88.79% <100.00%> (+0.09%) ⬆️
src/spatialdata_plot/pl/utils.py 68.33% <100.00%> (+0.50%) ⬆️
src/spatialdata_plot/pl/basic.py 79.46% <91.66%> (+0.42%) ⬆️

... 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 added 3 commits May 28, 2026 21:19
`compare()` previously forced every figure into a single 5-inch canvas with
constrained layout. For multi-panel figures (e.g. scanpy-style color=[...] or
multiple coordinate systems) this crammed the panels together and pushed each
panel's per-axis colorbar/legend into the neighbouring panel.

Only normalize size + layout for single-panel figures (count gridspec-placed
axes; colorbar/legend insets have no subplotspec). Multi-panel figures keep
their own size and spacing; the thumbnail is produced by the existing resize
step. Single-panel baselines are unaffected; multi-panel baselines are
regenerated.
These baselines now reflect the figures' natural per-panel layout (colorbars and
legends within each panel's column instead of overlapping neighbours). Includes
the new multi-panel color baselines and existing multi-panel tests (group
filtering, datashader reduction grid, multichannel normalization, frameon
multi-panel, raccoon split, visium hex grid, transparent-cmap comparisons).
Generated from the py3.11-stable CI artifact.
The compare() change traded dense-colorbar overlap for letterboxed whitespace
and, in some cases, new legend overlap, regressing existing multi-panel
baselines. The 400x300 comparison canvas fundamentally cannot render per-panel
colorbars/legends without crowding, so visual baselines are a poor fit for the
multi-panel color feature.

Revert compare() to its original behavior, restore the existing multi-panel
baselines, and drop the four overlapping multi-panel color baselines/visual
tests. The feature stays covered by structural tests: panel count + titles +
ncols geometry, per-panel categorical legends, shared scalar background,
scalar-form fallbacks, error cases, and branch independence.
@LucaMarconato
Copy link
Copy Markdown
Member

CCing @melonora, wasn't this feature available on a earlier version and then got refactored out because it was making linking parameters to color challenging?

But I think having all the elements in color= share the other parameters would be a good way to have this back.

@timtreis
Copy link
Copy Markdown
Member Author

Yeah, we removed it for type checking "elegance" but with the new ColorLike class it's cleaner now. And it's annoying for people to not have this ability in the native package.

The original plan was to upstream that ability to Squidpy, but there are so many plots you can only do with the non-wrapped sdata-plot that I figured it'd be easier to just allow it here again so people don't have to go upstream for this one specific feature

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.

Feature request: support color=["gene1","gene2"] for multi-panel and multi-channel spatial plots

3 participants