Skip to content

Commit f12292b

Browse files
timtreisclaude
andcommitted
Allow per-channel norms without explicit cmap
When norm is a list but no cmap is provided, auto-generate a default cmap list (one per norm) so the per-channel rendering path works. Users shouldn't need to write cmap=[plt.cm.gray]*3 just to use per-channel normalization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4eb263a commit f12292b

2 files changed

Lines changed: 15 additions & 16 deletions

File tree

src/spatialdata_plot/pl/basic.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ def render_images(
548548
Colormap normalization for continuous annotations, see :class:`matplotlib.colors.Normalize`.
549549
A single :class:`~matplotlib.colors.Normalize` applies to all channels.
550550
A list of :class:`~matplotlib.colors.Normalize` objects applies per-channel
551-
(length must match the number of colormaps/channels).
551+
(length must match the number of channels).
552552
na_color : ColorLike | None, default "default" (gets set to "lightgray")
553553
Color to be used for NAs values, if present. Can either be a named color ("red"), a hex representation
554554
("#000000ff") or a list of floats that represent RGB/RGBA values (1.0, 0.0, 0.0, 1.0). When None, the values
@@ -632,8 +632,14 @@ def render_images(
632632

633633
for element, param_values in params_dict.items():
634634
cmap_params: list[CmapParams] | CmapParams
635-
# Resolve which cmap to use for norm-list path vs scalar path.
635+
# Resolve which cmap to use for the norm-list path vs scalar path.
636636
effective_cmap = param_values.get("cmap") if isinstance(norm, list) else cmap
637+
638+
# When the user passes per-channel norms without explicit cmaps,
639+
# generate a default cmap list so the per-channel path works.
640+
if isinstance(norm, list) and len(norm) > 1 and not isinstance(effective_cmap, list):
641+
effective_cmap = [None] * len(norm)
642+
637643
if isinstance(effective_cmap, list) and len(effective_cmap) > 1:
638644
if isinstance(norm, list):
639645
if len(norm) != len(effective_cmap):
@@ -654,16 +660,7 @@ def render_images(
654660
]
655661

656662
else:
657-
if isinstance(norm, list):
658-
if len(norm) == 1:
659-
norm_scalar: Normalize | None = norm[0]
660-
else:
661-
raise ValueError(
662-
"When 'norm' is a list, you must also pass a list of colormaps via 'cmap' "
663-
"with matching length, or use a single Normalize."
664-
)
665-
else:
666-
norm_scalar = norm
663+
norm_scalar = norm[0] if isinstance(norm, list) else norm
667664
scalar_cmap = effective_cmap[0] if isinstance(effective_cmap, list) else cmap
668665
cmap_params = _prepare_cmap_norm(
669666
cmap=scalar_cmap,

tests/pl/test_render_images.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,13 @@ def test_norm_list_with_invalid_element_raises():
475475
sdata.pl.render_images("img", norm=["not_a_norm"]).pl.show()
476476

477477

478-
def test_norm_list_without_cmap_list_raises():
479-
"""Norm list requires explicit cmap list."""
478+
def test_norm_list_without_explicit_cmap():
479+
"""Per-channel norms work without explicit cmap (auto-assigns default cmap per channel)."""
480480
sdata = _make_multichannel_sdata()
481-
with pytest.raises(ValueError, match="must also pass a list of colormaps"):
482-
sdata.pl.render_images("img", norm=[Normalize(0, 1)] * 3).pl.show()
481+
norms = [Normalize(0, 0.05), Normalize(0, 1.0), Normalize(0, 0.5)]
482+
fig, ax = plt.subplots()
483+
sdata.pl.render_images("img", channel=[0, 1, 2], norm=norms).pl.show(ax=ax)
484+
plt.close(fig)
483485

484486

485487
def test_cmap_matches_selected_channels_not_full_image(sdata_blobs: SpatialData):

0 commit comments

Comments
 (0)