@@ -416,3 +416,78 @@ def test_no_clipping_warning_palette_compositing(self):
416416 plt .close ("all" )
417417 clip_warns = [x for x in w if "Clipping input data" in str (x .message )]
418418 assert len (clip_warns ) == 0 , f"Got unexpected clipping warning: { clip_warns [0 ].message } "
419+
420+
421+ def _make_multichannel_sdata ():
422+ """Create a 3-channel image with different intensity ranges."""
423+ rng = np .random .default_rng (42 )
424+ data = np .stack (
425+ [
426+ rng .uniform (0 , 0.05 , (50 , 50 )), # dim
427+ rng .uniform (0 , 1.0 , (50 , 50 )), # full range
428+ rng .uniform (0 , 0.5 , (50 , 50 )), # medium
429+ ],
430+ axis = 0 ,
431+ ).astype (np .float32 )
432+ img = Image2DModel .parse (data , dims = ("c" , "y" , "x" ), c_coords = [0 , 1 , 2 ])
433+ return SpatialData (images = {"img" : img })
434+
435+
436+ def test_per_channel_norm_list ():
437+ """Per-channel norm list is accepted and renders without error (#460)."""
438+ sdata = _make_multichannel_sdata ()
439+ norms = [
440+ Normalize (vmin = 0 , vmax = 0.05 , clip = True ),
441+ Normalize (vmin = 0 , vmax = 1.0 , clip = True ),
442+ Normalize (vmin = 0 , vmax = 0.5 , clip = True ),
443+ ]
444+ fig , ax = plt .subplots ()
445+ sdata .pl .render_images ("img" , channel = [0 , 1 , 2 ], norm = norms , cmap = [plt .cm .gray ] * 3 ).pl .show (ax = ax )
446+ plt .close (fig )
447+
448+
449+ def test_single_norm_with_multiple_channels ():
450+ """A single Normalize shared across channels still works."""
451+ sdata = _make_multichannel_sdata ()
452+ fig , ax = plt .subplots ()
453+ sdata .pl .render_images ("img" , channel = [0 , 1 , 2 ], norm = Normalize (0 , 1 ), cmap = [plt .cm .gray ] * 3 ).pl .show (ax = ax )
454+ plt .close (fig )
455+
456+
457+ def test_norm_list_length_mismatch_raises ():
458+ """Norm list length must match cmap list length."""
459+ sdata = _make_multichannel_sdata ()
460+ with pytest .raises (ValueError , match = "must match" ):
461+ sdata .pl .render_images ("img" , channel = [0 , 1 , 2 ], norm = [Normalize (0 , 1 )] * 2 , cmap = [plt .cm .gray ] * 3 ).pl .show ()
462+
463+
464+ def test_norm_list_empty_raises ():
465+ """Empty norm list is rejected."""
466+ sdata = _make_multichannel_sdata ()
467+ with pytest .raises (ValueError , match = "must not be empty" ):
468+ sdata .pl .render_images ("img" , norm = []).pl .show ()
469+
470+
471+ def test_norm_list_with_invalid_element_raises ():
472+ """Non-Normalize items in norm list are rejected."""
473+ sdata = _make_multichannel_sdata ()
474+ with pytest .raises (TypeError , match = "Normalize instance" ):
475+ sdata .pl .render_images ("img" , norm = ["not_a_norm" ]).pl .show ()
476+
477+
478+ def test_norm_list_without_explicit_cmap ():
479+ """Per-channel norms work without explicit cmap (auto-assigns default cmap per channel)."""
480+ sdata = _make_multichannel_sdata ()
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 )
485+
486+
487+ def test_cmap_matches_selected_channels_not_full_image (sdata_blobs : SpatialData ):
488+ """Cmap length should be validated against selected channels, not the full image channel count."""
489+ # blobs_image has 3 channels; select 1 with a matching length-1 cmap
490+ fig , ax = plt .subplots ()
491+ sdata_blobs .pl .render_images ("blobs_image" , channel = [0 ], cmap = ["gray" ]).pl .show (ax = ax )
492+ assert len (ax .get_images ()) == 1
493+ plt .close (fig )
0 commit comments