Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/spatialdata_plot/pl/_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def _validate_show_parameters(
dpi: int | None,
fig: Figure | None,
title: list[str] | str | None,
xlabel: str | None,
ylabel: str | None,
pad_extent: int | float,
ax: list[Axes] | Axes | None,
return_ax: bool,
Expand Down Expand Up @@ -183,6 +185,10 @@ def _validate_show_parameters(
if title is not None and not isinstance(title, list | str):
raise TypeError("Parameter 'title' must be a string or a list of strings.")

for _name, _val in (("xlabel", xlabel), ("ylabel", ylabel)):
if _val is not None and not isinstance(_val, str):
raise TypeError(f"Parameter '{_name}' must be a string or None.")

if not isinstance(pad_extent, int | float):
raise TypeError("Parameter 'pad_extent' must be numeric.")

Expand Down
25 changes: 22 additions & 3 deletions src/spatialdata_plot/pl/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,8 @@ def show(
dpi: int | None = None,
fig: Figure | None = None,
title: list[str] | str | None = None,
xlabel: str | None = None,
ylabel: str | None = None,
pad_extent: int | float = 0,
ax: list[Axes] | Axes | None = None,
return_ax: bool = False,
Expand Down Expand Up @@ -1378,6 +1380,10 @@ def show(
Title(s) for the plot. A single string is applied to all panels; a list must match the number
of panels. If ``None``, each panel is titled with its coordinate system name, or, in multi-panel
color mode, with its color key.
xlabel : str | None, default None
Label for the x axis, applied to every rendered panel. ``None`` leaves it unlabelled.
ylabel : str | None, default None
Label for the y axis, applied to every rendered panel. ``None`` leaves it unlabelled.
pad_extent : int | float, default 0
Padding added around the computed spatial extent on all sides.
ax : list[Axes] | Axes | None
Expand Down Expand Up @@ -1440,6 +1446,8 @@ def show(
dpi=dpi,
fig=fig,
title=title,
xlabel=xlabel,
ylabel=ylabel,
pad_extent=pad_extent,
ax=ax,
return_ax=return_ax,
Expand Down Expand Up @@ -1565,6 +1573,8 @@ def show(
axis_channel_legend_entries=axis_channel_legend_entries,
cs_row=cs_row,
title=title,
xlabel=xlabel,
ylabel=ylabel,
dpi=dpi,
figsize=figsize,
)
Expand Down Expand Up @@ -1982,15 +1992,18 @@ def _finalize_panel(
ax: Axes,
panel_idx: int,
title: list[str] | None,
xlabel: str | None,
ylabel: str | None,
panel_key: str | None,
cs: str,
frameon: bool | None,
) -> None:
"""Set a panel's title, equal aspect ratio and frame visibility.
"""Set a panel's title, axis labels, equal aspect ratio and frame visibility.

With no explicit ``title`` the panel is labelled with its color key (multi-panel color mode)
or its coordinate-system name; a single-element list applies to every panel, otherwise the
title at ``panel_idx`` is used.
title at ``panel_idx`` is used. ``xlabel``/``ylabel`` are applied to every panel; ``None``
leaves the respective axis unlabelled.
"""
if title is None:
t = panel_key if panel_key is not None else cs
Expand All @@ -2000,6 +2013,10 @@ def _finalize_panel(
# len(title) == num_panels is guaranteed by the up-front check in show().
t = title[panel_idx]
ax.set_title(t)
if xlabel is not None:
ax.set_xlabel(xlabel)
if ylabel is not None:
ax.set_ylabel(ylabel)
ax.set_aspect("equal")
if frameon is False:
ax.axis("off")
Expand Down Expand Up @@ -2056,6 +2073,8 @@ def _render_panel(
axis_channel_legend_entries: list[ChannelLegendEntry],
cs_row: pd.Series,
title: list[str] | None,
xlabel: str | None,
ylabel: str | None,
dpi: int | None,
figsize: tuple[float, float] | None,
) -> tuple[list[str], dict[str, bool]]:
Expand Down Expand Up @@ -2124,5 +2143,5 @@ def _render_panel(
_RENDERERS[cmd](**kwargs)

# Panel finalization depends only on per-panel values, so run it once after the loop.
_finalize_panel(ax, panel_idx, title, panel_key, cs, fig_params.frameon)
_finalize_panel(ax, panel_idx, title, xlabel, ylabel, panel_key, cs, fig_params.frameon)
return wanted_elements, wants
Binary file added tests/_images/Show_xlabel_ylabel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions tests/pl/test_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class TestShow(PlotTester, metaclass=PlotTesterMeta):
def test_plot_pad_extent_adds_padding(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image").pl.show(pad_extent=100)

def test_plot_xlabel_ylabel(self, sdata_blobs: SpatialData):
"""Visual test: xlabel/ylabel label the axes (feature for #763)."""
sdata_blobs.pl.render_images(element="blobs_image").pl.show(xlabel="x (µm)", ylabel="y (µm)")

def test_plot_frameon_false_single_panel(self, sdata_blobs: SpatialData):
"""Visual test: frameon=False hides axes decorations on a single panel (regression for #204)."""
sdata_blobs.pl.render_images(element="blobs_image").pl.show(frameon=False)
Expand Down Expand Up @@ -172,6 +176,41 @@ def test_title_count_validation(sdata_blobs: SpatialData):
plt.close("all")


def test_xlabel_ylabel(sdata_blobs: SpatialData):
"""xlabel/ylabel set the axis labels on every panel; None keeps them empty (feature for #763)."""
base = sdata_blobs.pl.render_images(element="blobs_image")

ax = base.pl.show(return_ax=True, show=False) # default: no labels
assert ax.get_xlabel() == "" and ax.get_ylabel() == ""
plt.close("all")

ax = base.pl.show(xlabel="x (µm)", ylabel="y (µm)", return_ax=True, show=False)
assert ax.get_xlabel() == "x (µm)" and ax.get_ylabel() == "y (µm)"
plt.close("all")

ax = base.pl.show(xlabel="µm", return_ax=True, show=False) # one axis only
assert ax.get_xlabel() == "µm" and ax.get_ylabel() == ""
plt.close("all")

# broadcast to every panel of a multi-panel plot
set_transformation(sdata_blobs["blobs_image"], Identity(), "second_cs")
axs = sdata_blobs.pl.render_images(element="blobs_image").pl.show(
xlabel="µm", ylabel="µm", return_ax=True, show=False
)
assert all(a.get_xlabel() == "µm" and a.get_ylabel() == "µm" for a in axs)
plt.close("all")


def test_xlabel_ylabel_validation(sdata_blobs: SpatialData):
"""xlabel/ylabel must each be a string or None (feature for #763)."""
base = sdata_blobs.pl.render_images(element="blobs_image")
with pytest.raises(TypeError, match="xlabel"):
base.pl.show(xlabel=1, show=False)
with pytest.raises(TypeError, match="ylabel"):
base.pl.show(ylabel=("y",), show=False)
plt.close("all")


def test_fig_parameter_warns_with_ax_list(sdata_blobs: SpatialData):
"""Passing fig= alongside a list of axes should also emit the deprecation (regression for #625)."""
set_transformation(sdata_blobs["blobs_image"], Identity(), "second_cs")
Expand Down