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
63 changes: 48 additions & 15 deletions src/spatialdata_plot/pl/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
)
from spatialdata_plot.pl.utils import (
_RENDER_CMD_TO_CS_FLAG,
_draw_scalebar,
_get_cs_contents,
_get_elements_to_be_rendered,
_get_valid_cs,
Expand Down Expand Up @@ -889,6 +890,10 @@ def show(
return_ax: bool = False,
save: str | Path | None = None,
show: bool | None = None,
scalebar_dx: float | None = None,
scalebar_units: str = "um",
scalebar_params: dict[str, Any] | None = None,
legend_params: dict[str, Any] | None = None,
) -> Axes | list[Axes] | None:
"""
Execute the plotting tree and display the final figure.
Expand Down Expand Up @@ -949,6 +954,22 @@ def show(
automatically when running in non-interactive mode (scripts) and suppressed in
interactive sessions (e.g. Jupyter). When ``ax`` is provided by the user, defaults
to ``False`` to allow further modifications.
scalebar_dx : float | None
Physical size of one axes-unit in ``scalebar_units``. If ``None``, no scalebar is drawn.
SpatialData coordinate systems carry no unit metadata, so this value must be supplied
explicitly (e.g. ``1.0`` when axes are already in micrometers; the microns-per-pixel
value when axes are in image pixels).
scalebar_units : str, default "um"
Unit string for the scalebar (passed to :class:`matplotlib_scalebar.scalebar.ScaleBar`).
Only takes effect when ``scalebar_dx`` is set.
scalebar_params : dict[str, Any] | None
Extra keyword arguments forwarded to :class:`matplotlib_scalebar.scalebar.ScaleBar`,
e.g. ``{"location": "lower right", "color": "white", "length_fraction": 0.25}``.
See the matplotlib-scalebar documentation for the full list of options.
legend_params : dict[str, Any] | None
Bundled legend options; overrides the matching ``legend_*`` flat kwargs. Accepted keys:
``location`` (or ``loc``), ``fontsize``, ``fontweight``, ``fontoutline``,
``na_in_legend``. Unknown keys raise ``ValueError``.

Returns
-------
Expand Down Expand Up @@ -986,6 +1007,10 @@ def show(
return_ax,
save,
show,
scalebar_dx,
scalebar_units,
scalebar_params,
legend_params,
)

if fig is not None and not isinstance(ax, Sequence):
Expand Down Expand Up @@ -1098,7 +1123,7 @@ def show(
raise ValueError(msg)

# set up canvas
fig_params, scalebar_params = _prepare_params_plot(
fig_params, scalebar_params_obj = _prepare_params_plot(
num_panels=len(coordinate_systems),
figsize=figsize,
dpi=dpi,
Expand All @@ -1108,15 +1133,25 @@ def show(
hspace=hspace,
ncols=ncols,
frameon=frameon,
scalebar_dx=scalebar_dx,
scalebar_units=scalebar_units,
scalebar_kwargs=scalebar_params,
)
legend_colorbar = colorbar
legend_params = LegendParams(
if legend_params:
legend_fontsize = legend_params.get("fontsize", legend_fontsize)
legend_fontweight = legend_params.get("fontweight", legend_fontweight)
# `loc` is matplotlib.Legend's native key; `location` aligns with colorbar/scalebar.
legend_loc = legend_params.get("location", legend_params.get("loc", legend_loc))
legend_fontoutline = legend_params.get("fontoutline", legend_fontoutline)
na_in_legend = legend_params.get("na_in_legend", na_in_legend)

legend_params_obj = LegendParams(
legend_fontsize=legend_fontsize,
legend_fontweight=legend_fontweight,
legend_loc=legend_loc,
legend_fontoutline=legend_fontoutline,
na_in_legend=na_in_legend,
colorbar=legend_colorbar,
colorbar=colorbar,
)

def _draw_colorbar(
Expand Down Expand Up @@ -1208,7 +1243,7 @@ def _draw_colorbar(
)
ax = fig_params.ax if fig_params.axs is None else fig_params.axs[i]
assert isinstance(ax, Axes)
axis_colorbar_requests: list[ColorbarSpec] | None = [] if legend_params.colorbar else None
axis_colorbar_requests: list[ColorbarSpec] | None = [] if legend_params_obj.colorbar else None
axis_channel_legend_entries: list[ChannelLegendEntry] = []

wants_images = False
Expand Down Expand Up @@ -1237,8 +1272,7 @@ def _draw_colorbar(
coordinate_system=cs,
ax=ax,
fig_params=fig_params,
scalebar_params=scalebar_params,
legend_params=legend_params,
legend_params=legend_params_obj,
colorbar_requests=axis_colorbar_requests,
channel_legend_entries=axis_channel_legend_entries,
rasterize=rasterize,
Expand All @@ -1256,8 +1290,7 @@ def _draw_colorbar(
coordinate_system=cs,
ax=ax,
fig_params=fig_params,
scalebar_params=scalebar_params,
legend_params=legend_params,
legend_params=legend_params_obj,
colorbar_requests=axis_colorbar_requests,
)

Expand All @@ -1273,8 +1306,7 @@ def _draw_colorbar(
coordinate_system=cs,
ax=ax,
fig_params=fig_params,
scalebar_params=scalebar_params,
legend_params=legend_params,
legend_params=legend_params_obj,
colorbar_requests=axis_colorbar_requests,
)

Expand Down Expand Up @@ -1309,8 +1341,7 @@ def _draw_colorbar(
coordinate_system=cs,
ax=ax,
fig_params=fig_params,
scalebar_params=scalebar_params,
legend_params=legend_params,
legend_params=legend_params_obj,
colorbar_requests=axis_colorbar_requests,
rasterize=rasterize,
)
Expand Down Expand Up @@ -1350,11 +1381,13 @@ def _draw_colorbar(
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_max, y_min) # (0, 0) is top-left

if legend_params.colorbar and axis_colorbar_requests:
if legend_params_obj.colorbar and axis_colorbar_requests:
pending_colorbars.append((ax, axis_colorbar_requests))

if axis_channel_legend_entries:
_draw_channel_legend(ax, axis_channel_legend_entries, legend_params, fig_params)
_draw_channel_legend(ax, axis_channel_legend_entries, legend_params_obj, fig_params)

_draw_scalebar(ax, scalebar_params_obj, panel_idx=i)

if pending_colorbars and fig_params.fig is not None:
fig = fig_params.fig
Expand Down
15 changes: 1 addition & 14 deletions src/spatialdata_plot/pl/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
LabelsRenderParams,
LegendParams,
PointsRenderParams,
ScalebarParams,
ShapesRenderParams,
)
from spatialdata_plot.pl.utils import (
Expand Down Expand Up @@ -268,9 +267,8 @@ def _add_legend_and_colorbar(
colorbar: bool | str | None,
colorbar_params: dict[str, object] | None,
colorbar_requests: list[ColorbarSpec] | None,
scalebar_params: ScalebarParams,
) -> None:
"""Add legend, colorbar, and scalebar decorations if the color vector warrants them."""
"""Add legend and colorbar decorations if the color vector warrants them."""
if not _want_decorations(color_vector, na_color):
return

Expand Down Expand Up @@ -309,8 +307,6 @@ def _add_legend_and_colorbar(
colorbar_params,
col_for_color if isinstance(col_for_color, str) else None,
),
scalebar_dx=scalebar_params.scalebar_dx,
scalebar_units=scalebar_params.scalebar_units,
)


Expand All @@ -320,7 +316,6 @@ def _render_shapes(
coordinate_system: str,
ax: matplotlib.axes.SubplotBase,
fig_params: FigParams,
scalebar_params: ScalebarParams,
legend_params: LegendParams,
colorbar_requests: list[ColorbarSpec] | None = None,
) -> None:
Expand Down Expand Up @@ -696,7 +691,6 @@ def _render_shapes(
colorbar=render_params.colorbar,
colorbar_params=render_params.colorbar_params,
colorbar_requests=colorbar_requests,
scalebar_params=scalebar_params,
)


Expand All @@ -706,7 +700,6 @@ def _render_points(
coordinate_system: str,
ax: matplotlib.axes.SubplotBase,
fig_params: FigParams,
scalebar_params: ScalebarParams,
legend_params: LegendParams,
colorbar_requests: list[ColorbarSpec] | None = None,
) -> None:
Expand Down Expand Up @@ -1050,7 +1043,6 @@ def _render_points(
colorbar=render_params.colorbar,
colorbar_params=render_params.colorbar_params,
colorbar_requests=colorbar_requests,
scalebar_params=scalebar_params,
)


Expand Down Expand Up @@ -1197,7 +1189,6 @@ def _render_images(
coordinate_system: str,
ax: matplotlib.axes.SubplotBase,
fig_params: FigParams,
scalebar_params: ScalebarParams,
legend_params: LegendParams,
rasterize: bool,
colorbar_requests: list[ColorbarSpec] | None = None,
Expand Down Expand Up @@ -1592,7 +1583,6 @@ def _render_labels(
coordinate_system: str,
ax: matplotlib.axes.SubplotBase,
fig_params: FigParams,
scalebar_params: ScalebarParams,
legend_params: LegendParams,
rasterize: bool,
colorbar_requests: list[ColorbarSpec] | None = None,
Expand Down Expand Up @@ -1821,7 +1811,4 @@ def _draw_labels(
render_params.colorbar_params,
col_for_color if isinstance(col_for_color, str) else None,
),
scalebar_dx=scalebar_params.scalebar_dx,
scalebar_units=scalebar_params.scalebar_units,
# scalebar_kwargs=scalebar_params.scalebar_kwargs,
)
7 changes: 4 additions & 3 deletions src/spatialdata_plot/pl/render_params.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Literal
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Literal

import numpy as np
from matplotlib.axes import Axes
Expand Down Expand Up @@ -220,6 +220,7 @@ class ScalebarParams:

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


@dataclass
Expand Down
55 changes: 46 additions & 9 deletions src/spatialdata_plot/pl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from copy import copy
from functools import partial
from pathlib import Path
from types import MappingProxyType
from typing import Any, Literal

import dask
Expand Down Expand Up @@ -273,6 +272,7 @@ def _prepare_params_plot(
# this args will be inferred from coordinate system
scalebar_dx: float | Sequence[float] | None = None,
scalebar_units: str | Sequence[str] | None = None,
scalebar_kwargs: Mapping[str, Any] | None = None,
) -> tuple[FigParams, ScalebarParams]:
# handle axes and size
wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 if wspace is None else wspace
Expand Down Expand Up @@ -325,11 +325,29 @@ def _prepare_params_plot(
num_panels=num_panels,
frameon=frameon,
)
scalebar_params = ScalebarParams(scalebar_dx=scalebar_dx, scalebar_units=scalebar_units)
scalebar_params = ScalebarParams(
scalebar_dx=scalebar_dx,
scalebar_units=scalebar_units,
scalebar_kwargs=dict(scalebar_kwargs) if scalebar_kwargs else {},
)

return fig_params, scalebar_params


def _draw_scalebar(ax: Axes, scalebar_params: ScalebarParams, panel_idx: int) -> None:
"""Attach a single :class:`matplotlib_scalebar.scalebar.ScaleBar` to ``ax``.

No-op when ``scalebar_dx`` is ``None``. ``scalebar_dx`` and ``scalebar_units`` are
broadcast lists indexed by the panel position; ``scalebar_kwargs`` is forwarded
verbatim to :class:`~matplotlib_scalebar.scalebar.ScaleBar`.
"""
if scalebar_params.scalebar_dx is None or scalebar_params.scalebar_units is None:
return
dx = scalebar_params.scalebar_dx[panel_idx]
units = scalebar_params.scalebar_units[panel_idx]
ax.add_artist(ScaleBar(dx, units=units, **scalebar_params.scalebar_kwargs))


def _get_cs_contents(sdata: sd.SpatialData) -> pd.DataFrame:
"""Check which coordinate systems contain which elements and return that info."""
cs_mapping = _get_coordinate_system_mapping(sdata)
Expand Down Expand Up @@ -1694,9 +1712,6 @@ def _decorate_axs(
colorbar_params: dict[str, object] | None = None,
colorbar_requests: list[ColorbarSpec] | None = None,
colorbar_label: str | None = None,
scalebar_dx: Sequence[float] | None = None,
scalebar_units: Sequence[str] | None = None,
scalebar_kwargs: Mapping[str, Any] = MappingProxyType({}),
) -> Axes:
if value_to_plot is not None:
# if only dots were plotted without an associated value
Expand Down Expand Up @@ -1743,10 +1758,6 @@ def _decorate_axs(
)
)

if isinstance(scalebar_dx, list) and isinstance(scalebar_units, list):
scalebar = ScaleBar(scalebar_dx, units=scalebar_units, **scalebar_kwargs)
ax.add_artist(scalebar)

return ax


Expand Down Expand Up @@ -2159,6 +2170,10 @@ def _validate_show_parameters(
return_ax: bool,
save: str | Path | None,
show: bool | None,
scalebar_dx: float | None,
scalebar_units: str,
scalebar_params: dict[str, Any] | None,
legend_params: dict[str, Any] | None,
) -> None:
if coordinate_systems is not None and not isinstance(coordinate_systems, list | str):
raise TypeError("Parameter 'coordinate_systems' must be a string or a list of strings.")
Expand Down Expand Up @@ -2248,6 +2263,28 @@ def _validate_show_parameters(
if show is not None and not isinstance(show, bool):
raise TypeError("Parameter 'show' must be a boolean or None.")

if scalebar_dx is not None:
if not isinstance(scalebar_dx, int | float) or isinstance(scalebar_dx, bool):
raise TypeError("Parameter 'scalebar_dx' must be a number or None.")
if scalebar_dx <= 0:
raise ValueError("Parameter 'scalebar_dx' must be > 0.")
if not isinstance(scalebar_units, str):
raise TypeError("Parameter 'scalebar_units' must be a string.")

if scalebar_params is not None and not isinstance(scalebar_params, dict):
raise TypeError("Parameter 'scalebar_params' must be a dictionary or None.")

if legend_params is not None:
if not isinstance(legend_params, dict):
raise TypeError("Parameter 'legend_params' must be a dictionary or None.")
# `loc` is matplotlib.Legend's native key; `location` aligns with colorbar_params / scalebar_params.
allowed_legend_keys = {"loc", "location", "fontsize", "fontweight", "fontoutline", "na_in_legend"}
unknown = set(legend_params) - allowed_legend_keys
if unknown:
raise ValueError(
f"Unknown legend_params key(s): {sorted(unknown)}. Allowed keys: {sorted(allowed_legend_keys)}."
)


def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[str, Any]:
colorbar = param_dict.get("colorbar", "auto")
Expand Down
Binary file added tests/_images/Show_scalebar_compact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/_images/Show_scalebar_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/_images/Show_scalebar_fixed_value_label.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/_images/Show_scalebar_no_frame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/_images/Show_scalebar_styled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading