Legends and colorbars in spatialdata-plot#

Every coloured plot needs a key. spatialdata-plot builds three kinds automatically: a categorical colour (named groups) gets a legend, a continuous colour (a numeric range) gets a colorbar, and a multichannel image gets a channel legend. show() places and styles the legend and colorbar; the channel legend is switched on with render_images(channels_as_legend=True). By the end you should be able to:

  • Place a categorical legend anywhere (legend_loc) and restyle it (legend_fontsize, legend_fontweight, legend_fontoutline, legend_title), or bundle those in legend_params.

  • Control which categories appear with na_in_legend.

  • Place and style a colorbar with colorbar / colorbar_params.

  • Label a multichannel composite with channels_as_legend.

  • Combine keys across a multi-panel figure.

We use the real 10x Visium mouse-brain dataset from squidpy for the legend and colorbar, and the Xenium cells fluorescence image for the channel legend — both cached on first use.

Setup#

import squidpy as sq

import spatialdata_plot  # noqa: F401  # registers the .pl accessor

sdata = sq.datasets.visium_hne_sdata()
sdata
INFO     Loading existing dataset from data/spatialdata/visium_hne_sdata.zarr
SpatialData object, with associated Zarr store: /Users/tim.treis/Documents/GitHub/spatialdata-plot/docs/notebooks/examples/data/spatialdata/visium_hne_sdata.zarr
├── Images
│     └── 'hne': DataTree[cyx] (3, 11757, 11291), (3, 5878, 5645), (3, 2939, 2822), (3, 1469, 1411)
├── Shapes
│     └── 'spots': GeoDataFrame shape: (2688, 2) (2D shapes)
└── Tables
      └── 'adata': AnnData (2688, 18078)
with coordinate systems:
    ▸ 'global', with elements:
        hne (Images), spots (Shapes)

1. The default categorical legend#

Colouring by a categorical column draws a legend automatically. cluster holds named brain regions; we render the spots over the H&E image.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="cluster")
).pl.show(legend_fontsize=6)
../../_images/eda530a810b3c8e0750f82658a4a9e9f93458b5d51d732a9854538acd3e0519a.png

2. Placing the legend — legend_loc#

legend_loc takes any matplotlib location ("upper right", "lower left", "center left", …) or "right margin" to park the legend outside the axes. The margin keeps the legend off the data; the matplotlib locations place it on top. Here are four placements of the same plot.

for loc in ["right margin", "upper right", "lower left", "center left"]:
    (
        sdata.pl.render_images("hne")
        .pl.render_shapes("spots", color="cluster")
    ).pl.show(legend_loc=loc, legend_fontsize=5, legend_title=loc)
../../_images/6d799d780aafb506581bd04a6c7240f5b16c61e225937603d441ed80bd9789f8.png ../../_images/51b8a44d5f4426268c1ae054d05a52fe6872452dd1401a75d18cc9ef34389f0e.png ../../_images/b3efaf67775658fb8030c198216c7411cc2076d1424da728913f67edc66b525b.png ../../_images/6c0c3352fd9759b2f3dcd656c5b050e4cb992b87c7fd5d7c326111952e95dcfa.png

3. Restyling the legend#

legend_fontsize, legend_fontweight, and legend_fontoutline (a contrasting outline around the text, useful over busy images) style the entries; legend_title names it.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="cluster")
).pl.show(
    legend_loc="lower left",
    legend_fontsize=6,
    legend_fontweight="bold",
    legend_fontoutline=2,
    legend_title="Region",
)
../../_images/6e19b49f14800d2f57e85657facabd85f04c23a55b452e25edc918162a404f48.png

4. Hiding NA — na_in_legend#

groups subsets a categorical colour to the named categories; the remaining spots become NA, and na_in_legend=False drops that NA entry from the key.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="cluster", groups=["Cortex_1", "Cortex_2", "Hippocampus"])
).pl.show(na_in_legend=False, legend_fontsize=7)
../../_images/8b7007806bf8e4cf3782c6386cc440d0c007027970186d24dbc48a55b51c527d.png

5. Bundling options — legend_params#

Instead of the flat legend_* kwargs you can pass one legend_params dict. Accepted keys: location (or loc), fontsize, fontweight, fontoutline, na_in_legend; it overrides the matching flat kwargs, and unknown keys raise an error.

Note legend_title is not among the accepted keys — keep it as a flat legend_title= kwarg.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="cluster")
).pl.show(
    legend_params={"loc": "right margin", "fontsize": 6, "fontweight": "bold"},
)
../../_images/eda530a810b3c8e0750f82658a4a9e9f93458b5d51d732a9854538acd3e0519a.png

6. Continuous colours get a colorbar#

Colouring by a continuous column draws a colorbar instead of a legend. Here, total counts per spot. colorbar=True is the default; pass colorbar=False to suppress it.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="total_counts")
).pl.show()
../../_images/e5b1603b54defc35e6f7bf1f355e8a1e4fbd9ba4cdd23898599504e66dbaccf1.png

7. Placing and styling the colorbar — colorbar_params#

colorbar_params accepts label, loc (position — "right" by default, or "left"/"top"/ "bottom"), width, pad, plus matplotlib colorbar keys. Set loc to move the bar; its orientation follows the side automatically (horizontal for top/bottom). Passing only orientation does not move it — the bar stays docked right.

(
    sdata.pl.render_images("hne")
    .pl.render_shapes("spots", color="total_counts")
).pl.show(
    colorbar_params={"loc": "bottom", "label": "Total counts"},
)
../../_images/f7619084b49ae7a0ef632f127a615f0d8293327824f01e06e0651de5b0c1b851.png

8. Labelling a multichannel image — channels_as_legend#

A multichannel image has no single colour to key, so channels_as_legend=True adds a channel legend mapping each channel to its rendered colour. We switch to the Xenium cells fluorescence image, whose channels are named markers (see the Multichannel & fluorescence images tutorial).

from spatialdata_plot import PercentileNormalize

cells = sq.datasets.cells()
n_channels = 4
cells.pl.render_images(
    "morphology_focus",
    norm=[PercentileNormalize(1, 99)] * n_channels,
    channels_as_legend=True,
).pl.show()
INFO     Loading existing dataset from data/spatialdata/cells.zarr
../../_images/5a016ada2dfcd15fb49e279bb51c21186235b03eec4d91339e24073343b4a7e0.png

9. Several keys in one figure#

Passing a list to color= renders one panel per colouring (see the Multi-panel colouring tutorial). Each panel gets the right key: a legend for the categorical panel, a colorbar for the continuous one.

(
    sdata.pl.render_shapes("spots", color=["cluster", "total_counts"])
).pl.show(ncols=2, legend_fontsize=5)
../../_images/2e6e35bc2b1ca00dfedb3aba7a124e8cdc0a8c935a69399048d3f635e8872b87.png

Summary#

  • Categorical colours draw a legend, continuous colours a colorbar, and multichannel images a channel legend (channels_as_legend) — all automatically.

  • Place the legend with legend_loc (matplotlib locations or "right margin"), and style it with legend_fontsize/fontweight/fontoutline, legend_title, na_in_legend; or bundle in legend_params.

  • Place and style the colorbar with colorbar_params; loc ("right"/"left"/"top"/"bottom") sets its position and the orientation follows.

  • In a multi-panel color=[...] figure each panel gets the appropriate key.

For reproducibility#

# ruff: noqa: F401, F811, I001, E402
# fmt: off
import warnings
import spatialdata_plot

%load_ext watermark
# fmt: on

%watermark -v -m -p spatialdata,spatialdata_plot,squidpy,matplotlib,numpy
Python implementation: CPython
Python version       : 3.14.6
IPython version      : 9.14.1

spatialdata     : 0.7.3
spatialdata_plot: 0.4.1
squidpy         : 1.8.2
matplotlib      : 3.11.0
numpy           : 2.4.6

Compiler    : Clang 20.1.8 
OS          : Darwin
Release     : 25.2.0
Machine     : arm64
Processor   : arm
CPU cores   : 8
Architecture: 64bit