Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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