Skip to content

Commit 85a171a

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/bbox-crop-show
# Conflicts: # tests/pl/test_show.py
2 parents b0e44f6 + fbb6872 commit 85a171a

4 files changed

Lines changed: 67 additions & 3 deletions

File tree

src/spatialdata_plot/pl/_validate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def _validate_show_parameters(
9696
dpi: int | None,
9797
fig: Figure | None,
9898
title: list[str] | str | None,
99+
xlabel: str | None,
100+
ylabel: str | None,
99101
pad_extent: int | float,
100102
crop_coord: tuple[float, float, float, float] | None,
101103
ax: list[Axes] | Axes | None,
@@ -184,6 +186,10 @@ def _validate_show_parameters(
184186
if title is not None and not isinstance(title, list | str):
185187
raise TypeError("Parameter 'title' must be a string or a list of strings.")
186188

189+
for _name, _val in (("xlabel", xlabel), ("ylabel", ylabel)):
190+
if _val is not None and not isinstance(_val, str):
191+
raise TypeError(f"Parameter '{_name}' must be a string or None.")
192+
187193
if not isinstance(pad_extent, int | float):
188194
raise TypeError("Parameter 'pad_extent' must be numeric.")
189195

src/spatialdata_plot/pl/basic.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,8 @@ def show(
13161316
dpi: int | None = None,
13171317
fig: Figure | None = None,
13181318
title: list[str] | str | None = None,
1319+
xlabel: str | None = None,
1320+
ylabel: str | None = None,
13191321
pad_extent: int | float = 0,
13201322
crop_coord: tuple[float, float, float, float] | None = None,
13211323
ax: list[Axes] | Axes | None = None,
@@ -1380,6 +1382,10 @@ def show(
13801382
Title(s) for the plot. A single string is applied to all panels; a list must match the number
13811383
of panels. If ``None``, each panel is titled with its coordinate system name, or, in multi-panel
13821384
color mode, with its color key.
1385+
xlabel : str | None, default None
1386+
Label for the x axis, applied to every rendered panel. ``None`` leaves it unlabelled.
1387+
ylabel : str | None, default None
1388+
Label for the y axis, applied to every rendered panel. ``None`` leaves it unlabelled.
13831389
pad_extent : int | float, default 0
13841390
Padding added around the computed spatial extent on all sides. Ignored when ``crop_coord`` is set.
13851391
crop_coord : tuple[float, float, float, float] | None, default None
@@ -1456,6 +1462,8 @@ def show(
14561462
dpi=dpi,
14571463
fig=fig,
14581464
title=title,
1465+
xlabel=xlabel,
1466+
ylabel=ylabel,
14591467
pad_extent=pad_extent,
14601468
crop_coord=crop_coord,
14611469
ax=ax,
@@ -1596,6 +1604,8 @@ def show(
15961604
axis_channel_legend_entries=axis_channel_legend_entries,
15971605
cs_row=cs_row,
15981606
title=title,
1607+
xlabel=xlabel,
1608+
ylabel=ylabel,
15991609
dpi=dpi,
16001610
figsize=figsize,
16011611
)
@@ -2021,15 +2031,18 @@ def _finalize_panel(
20212031
ax: Axes,
20222032
panel_idx: int,
20232033
title: list[str] | None,
2034+
xlabel: str | None,
2035+
ylabel: str | None,
20242036
panel_key: str | None,
20252037
cs: str,
20262038
frameon: bool | None,
20272039
) -> None:
2028-
"""Set a panel's title, equal aspect ratio and frame visibility.
2040+
"""Set a panel's title, axis labels, equal aspect ratio and frame visibility.
20292041
20302042
With no explicit ``title`` the panel is labelled with its color key (multi-panel color mode)
20312043
or its coordinate-system name; a single-element list applies to every panel, otherwise the
2032-
title at ``panel_idx`` is used.
2044+
title at ``panel_idx`` is used. ``xlabel``/``ylabel`` are applied to every panel; ``None``
2045+
leaves the respective axis unlabelled.
20332046
"""
20342047
if title is None:
20352048
t = panel_key if panel_key is not None else cs
@@ -2039,6 +2052,10 @@ def _finalize_panel(
20392052
# len(title) == num_panels is guaranteed by the up-front check in show().
20402053
t = title[panel_idx]
20412054
ax.set_title(t)
2055+
if xlabel is not None:
2056+
ax.set_xlabel(xlabel)
2057+
if ylabel is not None:
2058+
ax.set_ylabel(ylabel)
20422059
ax.set_aspect("equal")
20432060
if frameon is False:
20442061
ax.axis("off")
@@ -2095,6 +2112,8 @@ def _render_panel(
20952112
axis_channel_legend_entries: list[ChannelLegendEntry],
20962113
cs_row: pd.Series,
20972114
title: list[str] | None,
2115+
xlabel: str | None,
2116+
ylabel: str | None,
20982117
dpi: int | None,
20992118
figsize: tuple[float, float] | None,
21002119
) -> tuple[list[str], dict[str, bool]]:
@@ -2163,5 +2182,5 @@ def _render_panel(
21632182
_RENDERERS[cmd](**kwargs)
21642183

21652184
# Panel finalization depends only on per-panel values, so run it once after the loop.
2166-
_finalize_panel(ax, panel_idx, title, panel_key, cs, fig_params.frameon)
2185+
_finalize_panel(ax, panel_idx, title, xlabel, ylabel, panel_key, cs, fig_params.frameon)
21672186
return wanted_elements, wants
85.4 KB
Loading

tests/pl/test_show.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def test_plot_crop_layered_elements(self, sdata_blobs: SpatialData):
6060
.pl.show(crop_coord=(150, 400, 150, 400))
6161
)
6262

63+
def test_plot_xlabel_ylabel(self, sdata_blobs: SpatialData):
64+
"""Visual test: xlabel/ylabel label the axes (feature for #763)."""
65+
sdata_blobs.pl.render_images(element="blobs_image").pl.show(xlabel="x (µm)", ylabel="y (µm)")
66+
6367
def test_plot_frameon_false_single_panel(self, sdata_blobs: SpatialData):
6468
"""Visual test: frameon=False hides axes decorations on a single panel (regression for #204)."""
6569
sdata_blobs.pl.render_images(element="blobs_image").pl.show(frameon=False)
@@ -461,6 +465,41 @@ def test_title_count_validation(sdata_blobs: SpatialData):
461465
plt.close("all")
462466

463467

468+
def test_xlabel_ylabel(sdata_blobs: SpatialData):
469+
"""xlabel/ylabel set the axis labels on every panel; None keeps them empty (feature for #763)."""
470+
base = sdata_blobs.pl.render_images(element="blobs_image")
471+
472+
ax = base.pl.show(return_ax=True, show=False) # default: no labels
473+
assert ax.get_xlabel() == "" and ax.get_ylabel() == ""
474+
plt.close("all")
475+
476+
ax = base.pl.show(xlabel="x (µm)", ylabel="y (µm)", return_ax=True, show=False)
477+
assert ax.get_xlabel() == "x (µm)" and ax.get_ylabel() == "y (µm)"
478+
plt.close("all")
479+
480+
ax = base.pl.show(xlabel="µm", return_ax=True, show=False) # one axis only
481+
assert ax.get_xlabel() == "µm" and ax.get_ylabel() == ""
482+
plt.close("all")
483+
484+
# broadcast to every panel of a multi-panel plot
485+
set_transformation(sdata_blobs["blobs_image"], Identity(), "second_cs")
486+
axs = sdata_blobs.pl.render_images(element="blobs_image").pl.show(
487+
xlabel="µm", ylabel="µm", return_ax=True, show=False
488+
)
489+
assert all(a.get_xlabel() == "µm" and a.get_ylabel() == "µm" for a in axs)
490+
plt.close("all")
491+
492+
493+
def test_xlabel_ylabel_validation(sdata_blobs: SpatialData):
494+
"""xlabel/ylabel must each be a string or None (feature for #763)."""
495+
base = sdata_blobs.pl.render_images(element="blobs_image")
496+
with pytest.raises(TypeError, match="xlabel"):
497+
base.pl.show(xlabel=1, show=False)
498+
with pytest.raises(TypeError, match="ylabel"):
499+
base.pl.show(ylabel=("y",), show=False)
500+
plt.close("all")
501+
502+
464503
def test_fig_parameter_warns_with_ax_list(sdata_blobs: SpatialData):
465504
"""Passing fig= alongside a list of axes should also emit the deprecation (regression for #625)."""
466505
set_transformation(sdata_blobs["blobs_image"], Identity(), "second_cs")

0 commit comments

Comments
 (0)