Skip to content

Commit cc5929d

Browse files
timtreisclaude
andcommitted
Fix shared Normalize causing out-of-range values in multi-channel rendering
The root cause of matplotlib's "Clipping input data" warning was that a single Normalize instance was shared across all channels. It auto-ranged on the first channel's min/max, causing subsequent channels with different value ranges to normalize outside [0, 1]. Fix: when the shared norm has auto-ranging (vmin=None or vmax=None), copy it per channel so each channel normalizes independently. This replaces the previous approach of clipping the final array. Additive compositing paths (palette, categorical) still clip after summing, since summing RGBA values can inherently exceed [0, 1]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dc490c9 commit cc5929d

1 file changed

Lines changed: 11 additions & 7 deletions

File tree

src/spatialdata_plot/pl/render.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,10 @@ def _render_images(
12011201
ch_norm = render_params.cmap_params[ch_idx].norm
12021202
else:
12031203
ch_norm = render_params.cmap_params.norm
1204+
# When a single auto-ranging norm is shared across channels, copy it so
1205+
# each channel normalizes independently based on its own value range.
1206+
if isinstance(ch_norm, Normalize) and (ch_norm.vmin is None or ch_norm.vmax is None):
1207+
ch_norm = copy(ch_norm)
12041208

12051209
if ch_norm is not None:
12061210
layers[ch] = ch_norm(layers[ch])
@@ -1229,7 +1233,7 @@ def _render_images(
12291233
)
12301234

12311235
_ax_show_and_transform(
1232-
np.clip(stacked, 0, 1),
1236+
stacked,
12331237
trans_data,
12341238
ax,
12351239
render_params.alpha,
@@ -1246,15 +1250,15 @@ def _render_images(
12461250
[channel_cmaps[ch_ind](layers[ch]) for ch_ind, ch in enumerate(channels)],
12471251
0,
12481252
).sum(0)
1249-
colored = colored[:, :, :3]
1253+
colored = np.clip(colored[:, :, :3], 0, 1)
12501254
elif n_channels == 3:
12511255
seed_colors = _get_colors_for_categorical_obs(list(range(n_channels)))
12521256
channel_cmaps = [_get_linear_colormap([c], "k")[0] for c in seed_colors]
12531257
colored = np.stack(
12541258
[channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)],
12551259
0,
12561260
).sum(0)
1257-
colored = colored[:, :, :3]
1261+
colored = np.clip(colored[:, :, :3], 0, 1)
12581262
else:
12591263
if isinstance(render_params.cmap_params, list):
12601264
cmap_is_default = render_params.cmap_params[0].cmap_is_default
@@ -1291,7 +1295,7 @@ def _render_images(
12911295
) # TODO: update when pca is added as strategy
12921296

12931297
_ax_show_and_transform(
1294-
np.clip(colored, 0, 1),
1298+
colored,
12951299
trans_data,
12961300
ax,
12971301
render_params.alpha,
@@ -1305,10 +1309,10 @@ def _render_images(
13051309

13061310
channel_cmaps = [_get_linear_colormap([c], "k")[0] for c in palette if isinstance(c, str)]
13071311
colored = np.stack([channel_cmaps[i](layers[c]) for i, c in enumerate(channels)], 0).sum(0)
1308-
colored = colored[:, :, :3]
1312+
colored = np.clip(colored[:, :, :3], 0, 1)
13091313

13101314
_ax_show_and_transform(
1311-
np.clip(colored, 0, 1),
1315+
colored,
13121316
trans_data,
13131317
ax,
13141318
render_params.alpha,
@@ -1327,7 +1331,7 @@ def _render_images(
13271331
colored = colored[:, :, :3]
13281332

13291333
_ax_show_and_transform(
1330-
np.clip(colored, 0, 1),
1334+
colored,
13311335
trans_data,
13321336
ax,
13331337
render_params.alpha,

0 commit comments

Comments
 (0)