Skip to content

Commit 09054db

Browse files
committed
Wire scalebar_dx/scalebar_units into pl.show() (closes #614)
The scalebar machinery (ScalebarParams, _get_scalebar, ScaleBar import) was present but unreachable: show() did not declare or forward scalebar_dx / scalebar_units, so any user attempt raised TypeError. The downstream ScaleBar(dx=[...], units=[...]) call in _add_decorations_to_ax also passed broadcast lists where ScaleBar requires scalars, and ran once per render layer (so multi-layer plots would have stacked duplicates). - Add scalebar_dx, scalebar_units (default "um"), and scalebar_params (kwargs dict, mirroring colorbar_params) to show()'s signature. - Centralize drawing in _draw_scalebar(ax, params, panel_idx); call it once per axis at the tail of show()'s panel loop. - Drop the broken scalebar block + scalebar_dx/units kwargs from _add_decorations_to_ax; drop the now-unused scalebar_params arg from _render_{shapes,points,images,labels} and _add_legend_and_colorbar. - Validate scalebar_dx/units/params types and sign in _validate_show_parameters. - Add 9 non-visual regression tests + 2 visual tests (default and styled).
1 parent e1f8863 commit 09054db

5 files changed

Lines changed: 173 additions & 31 deletions

File tree

src/spatialdata_plot/pl/basic.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
)
5555
from spatialdata_plot.pl.utils import (
5656
_RENDER_CMD_TO_CS_FLAG,
57+
_draw_scalebar,
5758
_get_cs_contents,
5859
_get_elements_to_be_rendered,
5960
_get_valid_cs,
@@ -889,6 +890,9 @@ def show(
889890
return_ax: bool = False,
890891
save: str | Path | None = None,
891892
show: bool | None = None,
893+
scalebar_dx: float | None = None,
894+
scalebar_units: str = "um",
895+
scalebar_params: dict[str, Any] | None = None,
892896
) -> Axes | list[Axes] | None:
893897
"""
894898
Execute the plotting tree and display the final figure.
@@ -949,6 +953,18 @@ def show(
949953
automatically when running in non-interactive mode (scripts) and suppressed in
950954
interactive sessions (e.g. Jupyter). When ``ax`` is provided by the user, defaults
951955
to ``False`` to allow further modifications.
956+
scalebar_dx : float | None
957+
Physical size of one axes-unit in ``scalebar_units``. If ``None``, no scalebar is drawn.
958+
SpatialData coordinate systems carry no unit metadata, so this value must be supplied
959+
explicitly (e.g. ``1.0`` when axes are already in micrometers; the microns-per-pixel
960+
value when axes are in image pixels).
961+
scalebar_units : str, default "um"
962+
Unit string for the scalebar (passed to :class:`matplotlib_scalebar.scalebar.ScaleBar`).
963+
Only takes effect when ``scalebar_dx`` is set.
964+
scalebar_params : dict[str, Any] | None
965+
Extra keyword arguments forwarded to :class:`matplotlib_scalebar.scalebar.ScaleBar`,
966+
e.g. ``{"location": "lower right", "color": "white", "length_fraction": 0.25}``.
967+
See the matplotlib-scalebar documentation for the full list of options.
952968
953969
Returns
954970
-------
@@ -986,6 +1002,9 @@ def show(
9861002
return_ax,
9871003
save,
9881004
show,
1005+
scalebar_dx,
1006+
scalebar_units,
1007+
scalebar_params,
9891008
)
9901009

9911010
if fig is not None and not isinstance(ax, Sequence):
@@ -1098,7 +1117,7 @@ def show(
10981117
raise ValueError(msg)
10991118

11001119
# set up canvas
1101-
fig_params, scalebar_params = _prepare_params_plot(
1120+
fig_params, scalebar_params_obj = _prepare_params_plot(
11021121
num_panels=len(coordinate_systems),
11031122
figsize=figsize,
11041123
dpi=dpi,
@@ -1108,6 +1127,9 @@ def show(
11081127
hspace=hspace,
11091128
ncols=ncols,
11101129
frameon=frameon,
1130+
scalebar_dx=scalebar_dx,
1131+
scalebar_units=scalebar_units,
1132+
scalebar_kwargs=scalebar_params,
11111133
)
11121134
legend_colorbar = colorbar
11131135
legend_params = LegendParams(
@@ -1237,7 +1259,6 @@ def _draw_colorbar(
12371259
coordinate_system=cs,
12381260
ax=ax,
12391261
fig_params=fig_params,
1240-
scalebar_params=scalebar_params,
12411262
legend_params=legend_params,
12421263
colorbar_requests=axis_colorbar_requests,
12431264
channel_legend_entries=axis_channel_legend_entries,
@@ -1256,7 +1277,6 @@ def _draw_colorbar(
12561277
coordinate_system=cs,
12571278
ax=ax,
12581279
fig_params=fig_params,
1259-
scalebar_params=scalebar_params,
12601280
legend_params=legend_params,
12611281
colorbar_requests=axis_colorbar_requests,
12621282
)
@@ -1273,7 +1293,6 @@ def _draw_colorbar(
12731293
coordinate_system=cs,
12741294
ax=ax,
12751295
fig_params=fig_params,
1276-
scalebar_params=scalebar_params,
12771296
legend_params=legend_params,
12781297
colorbar_requests=axis_colorbar_requests,
12791298
)
@@ -1309,7 +1328,6 @@ def _draw_colorbar(
13091328
coordinate_system=cs,
13101329
ax=ax,
13111330
fig_params=fig_params,
1312-
scalebar_params=scalebar_params,
13131331
legend_params=legend_params,
13141332
colorbar_requests=axis_colorbar_requests,
13151333
rasterize=rasterize,
@@ -1356,6 +1374,8 @@ def _draw_colorbar(
13561374
if axis_channel_legend_entries:
13571375
_draw_channel_legend(ax, axis_channel_legend_entries, legend_params, fig_params)
13581376

1377+
_draw_scalebar(ax, scalebar_params_obj, panel_idx=i)
1378+
13591379
if pending_colorbars and fig_params.fig is not None:
13601380
fig = fig_params.fig
13611381
fig.canvas.draw()

src/spatialdata_plot/pl/render.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
LabelsRenderParams,
5454
LegendParams,
5555
PointsRenderParams,
56-
ScalebarParams,
5756
ShapesRenderParams,
5857
)
5958
from spatialdata_plot.pl.utils import (
@@ -268,9 +267,8 @@ def _add_legend_and_colorbar(
268267
colorbar: bool | str | None,
269268
colorbar_params: dict[str, object] | None,
270269
colorbar_requests: list[ColorbarSpec] | None,
271-
scalebar_params: ScalebarParams,
272270
) -> None:
273-
"""Add legend, colorbar, and scalebar decorations if the color vector warrants them."""
271+
"""Add legend and colorbar decorations if the color vector warrants them."""
274272
if not _want_decorations(color_vector, na_color):
275273
return
276274

@@ -309,8 +307,6 @@ def _add_legend_and_colorbar(
309307
colorbar_params,
310308
col_for_color if isinstance(col_for_color, str) else None,
311309
),
312-
scalebar_dx=scalebar_params.scalebar_dx,
313-
scalebar_units=scalebar_params.scalebar_units,
314310
)
315311

316312

@@ -320,7 +316,6 @@ def _render_shapes(
320316
coordinate_system: str,
321317
ax: matplotlib.axes.SubplotBase,
322318
fig_params: FigParams,
323-
scalebar_params: ScalebarParams,
324319
legend_params: LegendParams,
325320
colorbar_requests: list[ColorbarSpec] | None = None,
326321
) -> None:
@@ -696,7 +691,6 @@ def _render_shapes(
696691
colorbar=render_params.colorbar,
697692
colorbar_params=render_params.colorbar_params,
698693
colorbar_requests=colorbar_requests,
699-
scalebar_params=scalebar_params,
700694
)
701695

702696

@@ -706,7 +700,6 @@ def _render_points(
706700
coordinate_system: str,
707701
ax: matplotlib.axes.SubplotBase,
708702
fig_params: FigParams,
709-
scalebar_params: ScalebarParams,
710703
legend_params: LegendParams,
711704
colorbar_requests: list[ColorbarSpec] | None = None,
712705
) -> None:
@@ -1050,7 +1043,6 @@ def _render_points(
10501043
colorbar=render_params.colorbar,
10511044
colorbar_params=render_params.colorbar_params,
10521045
colorbar_requests=colorbar_requests,
1053-
scalebar_params=scalebar_params,
10541046
)
10551047

10561048

@@ -1197,7 +1189,6 @@ def _render_images(
11971189
coordinate_system: str,
11981190
ax: matplotlib.axes.SubplotBase,
11991191
fig_params: FigParams,
1200-
scalebar_params: ScalebarParams,
12011192
legend_params: LegendParams,
12021193
rasterize: bool,
12031194
colorbar_requests: list[ColorbarSpec] | None = None,
@@ -1592,7 +1583,6 @@ def _render_labels(
15921583
coordinate_system: str,
15931584
ax: matplotlib.axes.SubplotBase,
15941585
fig_params: FigParams,
1595-
scalebar_params: ScalebarParams,
15961586
legend_params: LegendParams,
15971587
rasterize: bool,
15981588
colorbar_requests: list[ColorbarSpec] | None = None,
@@ -1821,7 +1811,4 @@ def _draw_labels(
18211811
render_params.colorbar_params,
18221812
col_for_color if isinstance(col_for_color, str) else None,
18231813
),
1824-
scalebar_dx=scalebar_params.scalebar_dx,
1825-
scalebar_units=scalebar_params.scalebar_units,
1826-
# scalebar_kwargs=scalebar_params.scalebar_kwargs,
18271814
)

src/spatialdata_plot/pl/render_params.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from collections.abc import Callable, Sequence
4-
from dataclasses import dataclass
5-
from typing import Literal
3+
from collections.abc import Callable, Mapping, Sequence
4+
from dataclasses import dataclass, field
5+
from typing import Any, Literal
66

77
import numpy as np
88
from matplotlib.axes import Axes
@@ -220,6 +220,7 @@ class ScalebarParams:
220220

221221
scalebar_dx: Sequence[float] | None = None
222222
scalebar_units: Sequence[str] | None = None
223+
scalebar_kwargs: Mapping[str, Any] = field(default_factory=dict)
223224

224225

225226
@dataclass

src/spatialdata_plot/pl/utils.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from copy import copy
88
from functools import partial
99
from pathlib import Path
10-
from types import MappingProxyType
1110
from typing import Any, Literal
1211

1312
import dask
@@ -273,6 +272,7 @@ def _prepare_params_plot(
273272
# this args will be inferred from coordinate system
274273
scalebar_dx: float | Sequence[float] | None = None,
275274
scalebar_units: str | Sequence[str] | None = None,
275+
scalebar_kwargs: Mapping[str, Any] | None = None,
276276
) -> tuple[FigParams, ScalebarParams]:
277277
# handle axes and size
278278
wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 if wspace is None else wspace
@@ -325,11 +325,29 @@ def _prepare_params_plot(
325325
num_panels=num_panels,
326326
frameon=frameon,
327327
)
328-
scalebar_params = ScalebarParams(scalebar_dx=scalebar_dx, scalebar_units=scalebar_units)
328+
scalebar_params = ScalebarParams(
329+
scalebar_dx=scalebar_dx,
330+
scalebar_units=scalebar_units,
331+
scalebar_kwargs=dict(scalebar_kwargs) if scalebar_kwargs else {},
332+
)
329333

330334
return fig_params, scalebar_params
331335

332336

337+
def _draw_scalebar(ax: Axes, scalebar_params: ScalebarParams, panel_idx: int) -> None:
338+
"""Attach a single :class:`matplotlib_scalebar.scalebar.ScaleBar` to ``ax``.
339+
340+
No-op when ``scalebar_dx`` is ``None``. ``scalebar_dx`` and ``scalebar_units`` are
341+
broadcast lists indexed by the panel position; ``scalebar_kwargs`` is forwarded
342+
verbatim to :class:`~matplotlib_scalebar.scalebar.ScaleBar`.
343+
"""
344+
if scalebar_params.scalebar_dx is None or scalebar_params.scalebar_units is None:
345+
return
346+
dx = scalebar_params.scalebar_dx[panel_idx]
347+
units = scalebar_params.scalebar_units[panel_idx]
348+
ax.add_artist(ScaleBar(dx, units=units, **scalebar_params.scalebar_kwargs))
349+
350+
333351
def _get_cs_contents(sdata: sd.SpatialData) -> pd.DataFrame:
334352
"""Check which coordinate systems contain which elements and return that info."""
335353
cs_mapping = _get_coordinate_system_mapping(sdata)
@@ -1694,9 +1712,6 @@ def _decorate_axs(
16941712
colorbar_params: dict[str, object] | None = None,
16951713
colorbar_requests: list[ColorbarSpec] | None = None,
16961714
colorbar_label: str | None = None,
1697-
scalebar_dx: Sequence[float] | None = None,
1698-
scalebar_units: Sequence[str] | None = None,
1699-
scalebar_kwargs: Mapping[str, Any] = MappingProxyType({}),
17001715
) -> Axes:
17011716
if value_to_plot is not None:
17021717
# if only dots were plotted without an associated value
@@ -1743,10 +1758,6 @@ def _decorate_axs(
17431758
)
17441759
)
17451760

1746-
if isinstance(scalebar_dx, list) and isinstance(scalebar_units, list):
1747-
scalebar = ScaleBar(scalebar_dx, units=scalebar_units, **scalebar_kwargs)
1748-
ax.add_artist(scalebar)
1749-
17501761
return ax
17511762

17521763

@@ -2159,6 +2170,9 @@ def _validate_show_parameters(
21592170
return_ax: bool,
21602171
save: str | Path | None,
21612172
show: bool | None,
2173+
scalebar_dx: float | None,
2174+
scalebar_units: str,
2175+
scalebar_params: dict[str, Any] | None,
21622176
) -> None:
21632177
if coordinate_systems is not None and not isinstance(coordinate_systems, list | str):
21642178
raise TypeError("Parameter 'coordinate_systems' must be a string or a list of strings.")
@@ -2248,6 +2262,17 @@ def _validate_show_parameters(
22482262
if show is not None and not isinstance(show, bool):
22492263
raise TypeError("Parameter 'show' must be a boolean or None.")
22502264

2265+
if scalebar_dx is not None:
2266+
if not isinstance(scalebar_dx, int | float) or isinstance(scalebar_dx, bool):
2267+
raise TypeError("Parameter 'scalebar_dx' must be a number or None.")
2268+
if scalebar_dx <= 0:
2269+
raise ValueError("Parameter 'scalebar_dx' must be > 0.")
2270+
if not isinstance(scalebar_units, str):
2271+
raise TypeError("Parameter 'scalebar_units' must be a string.")
2272+
2273+
if scalebar_params is not None and not isinstance(scalebar_params, dict):
2274+
raise TypeError("Parameter 'scalebar_params' must be a dictionary or None.")
2275+
22512276

22522277
def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[str, Any]:
22532278
colorbar = param_dict.get("colorbar", "auto")

0 commit comments

Comments
 (0)