Skip to content

channels_as_legend=True silently ignored for single-channel and sequential renders #601

@timtreis

Description

@timtreis

channels_as_legend=True silently ignored for single-channel renders

The most common image render pattern produces no legend even when channels_as_legend=True is set.


Environment

  • spatialdata-plot 0.3.4.dev (main, commit 5cfedc7)
  • spatialdata 0.5.0
  • Python 3.13

Problem

render_images(..., channels_as_legend=True) is silently ignored in the two most common rendering scenarios:

  1. Single-channel render (channel="DAPI"): the single-channel code path returns before reaching the channels_as_legend block, which lives exclusively inside the else (n_channels > 1) branch.

  2. Sequential single-channel renders (common pattern for channel overlays):

    sdata.pl.render_images("img", channel="DAPI", channels_as_legend=True)
            .pl.render_images("img", channel="GFP",  channels_as_legend=True)
            .pl.show()

    Each call renders 1 channel and goes through the single-channel path, so channel_legend_entries is never populated and no legend appears.

  3. True RGB images (channel coords "r", "g", "b"): an early return at the RGB path bypasses channels_as_legend entirely.

No error or warning is emitted in any of these cases. Users do not know whether the legend is missing because of a bug or because of their render configuration.

channels_as_legend was added in #576 for the multi-channel composite path, but the single-channel path was not updated.


Minimal reproducible example

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import dask
dask.config.set({"dataframe.query-planning": False})
import spatialdata as sd
from spatialdata.models import Image2DModel
import spatialdata_plot

img_arr = np.random.rand(3, 20, 20).astype(np.float32)
image = Image2DModel.parse(img_arr, dims=["c","y","x"], c_coords=["DAPI","GFP","RFP"])
sdata = sd.SpatialData(images={"img": image})

# Case 1: single-channel render
fig1, ax1 = plt.subplots()
sdata.pl.render_images("img", channel="DAPI", channels_as_legend=True).pl.show(ax=ax1)
print(f"Single channel legend: {ax1.get_legend()}")  # None

# Case 2: two sequential single-channel renders
fig2, ax2 = plt.subplots()
(sdata.pl.render_images("img", channel="DAPI", channels_as_legend=True)
         .pl.render_images("img", channel="GFP", channels_as_legend=True)
         .pl.show(ax=ax2))
print(f"Two single-channel renders legend: {ax2.get_legend()}")  # None

Expected behaviour

After render_images("img", channel="DAPI", channels_as_legend=True), the axes should have a legend entry for "DAPI" using the color applied to that channel.

After two sequential single-channel renders with channels_as_legend=True, the legend should list both "DAPI" and "GFP".

Actual behaviour

Single channel legend: None
Two single-channel renders legend: None

Fix sketch

In the single-channel code path, collect the legend entry after rendering:

if render_params.channels_as_legend and channel_legend_entries is not None:
    channel_color = <color used for this channel>   # from cmap or palette
    channel_legend_entries.append((channel, channel_color))

For the RGB early-return path, add a similar collection step for ["r", "g", "b"] with ["red", "green", "blue"] (or the actual colors used) before the return.

At minimum, add a UserWarning in both single-channel and RGB paths when channels_as_legend=True is set but has no effect, so users get actionable feedback.


Related


Triage tier: Tier 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions