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
24 changes: 24 additions & 0 deletions src/spatialdata_plot/pl/_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import warnings
from collections.abc import Mapping, Sequence
from copy import copy
from dataclasses import dataclass, replace
Expand Down Expand Up @@ -312,6 +313,29 @@ def _set_outline(
)


def _resolve_outline_toggle(
outline: bool | None,
outline_alpha: float | int | tuple[float | int, float | int] | None,
detail_params_set: bool,
) -> float | int | tuple[float | int, float | int] | None:
"""Map the tri-state ``outline`` toggle onto an effective ``outline_alpha``.

``None`` leaves it unchanged; ``True`` forces on (1.0 if unset/zero); ``False`` forces off and warns on conflict.
"""
if outline is None:
return outline_alpha
alpha_is_zero = outline_alpha is None or bool(np.all(np.asarray(outline_alpha, dtype=float) == 0.0))
if outline:
return 1.0 if alpha_is_zero else outline_alpha
if detail_params_set or not alpha_is_zero:
warnings.warn(
"`outline=False` overrides the `outline_*` parameters you set; no outline will be drawn.",
UserWarning,
stacklevel=3,
)
return 0.0


def _get_colors_for_categorical_obs(
categories: Sequence[str | int],
palette: ListedColormap | str | list[str] | None = None,
Expand Down
15 changes: 15 additions & 0 deletions src/spatialdata_plot/pl/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
_maybe_set_colors,
_next_palette_colors,
_prepare_cmap_norm,
_resolve_outline_toggle,
_set_outline,
)
from spatialdata_plot.pl._validate import (
Expand Down Expand Up @@ -325,6 +326,7 @@ def render_shapes(
groups: list[str] | str | None = None,
palette: dict[str, str] | list[str] | str | None = None,
na_color: ColorLike | None = "default",
outline: bool | None = None,
outline_width: float | int | tuple[float | int, float | int] | None = None,
outline_color: ColorLike | tuple[ColorLike] | None = None,
outline_alpha: float | int | tuple[float | int, float | int] | None = None,
Expand Down Expand Up @@ -388,6 +390,10 @@ def render_shapes(
elements are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
outline : bool | None, default None
Convenience on/off switch. ``None`` infers visibility from the ``outline_*`` params;
``True`` forces an outline on (defaults fill in any unset width/color/alpha); ``False``
forces it off, overriding and warning about any ``outline_*`` you set.
outline_width : float | int | tuple[float | int, float | int], optional
Width of the border. If 2 values are given (tuple), 2 borders are shown with these widths (outer & inner).
If `outline_color` and/or `outline_alpha` are used to indicate that one/two outlines should be drawn, the
Expand Down Expand Up @@ -466,6 +472,9 @@ def render_shapes(
"""
if as_points:
_validate_as_points_size(size)
outline_alpha = _resolve_outline_toggle(
outline, outline_alpha, outline_width is not None or outline_color is not None
)
panel_param_dicts = _expand_color_panels(
self._sdata,
color,
Expand Down Expand Up @@ -968,6 +977,7 @@ def render_labels(
cmap: Colormap | str | None = None,
norm: Normalize | None = None,
na_color: ColorLike | None = "default",
outline: bool | None = None,
outline_alpha: float | int = 0.0,
fill_alpha: float | int | None = None,
outline_color: ColorLike | None = None,
Expand Down Expand Up @@ -1029,6 +1039,10 @@ def render_labels(
labels are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
outline : bool | None, default None
Convenience on/off switch; thickness is set by ``contour_px``. ``None`` infers visibility from
``outline_color``/``outline_alpha``; ``True`` forces on (alpha defaults to 1.0); ``False``
forces it off, overriding and warning about any params you set.
outline_alpha : float | int, default 0.0
Alpha value for the outline of the labels. Invisible by default.
fill_alpha : float | int | None, optional
Expand Down Expand Up @@ -1078,6 +1092,7 @@ def render_labels(
"""
if as_points:
_validate_as_points_size(size)
outline_alpha = cast("float | int", _resolve_outline_toggle(outline, outline_alpha, outline_color is not None))
panel_param_dicts = _expand_color_panels(
self._sdata,
color,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions tests/pl/test_render_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,15 @@ def test_plot_can_control_label_outline(self, sdata_blobs: SpatialData):
contour_px=15,
).pl.show()

def test_plot_outline_toggle_true_draws_outline(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_labels("blobs_labels", outline=True, fill_alpha=0.0, contour_px=15).pl.show()

def test_plot_outline_toggle_false_suppresses_outline(self, sdata_blobs: SpatialData):
with pytest.warns(UserWarning, match="outline=False"):
sdata_blobs.pl.render_labels(
"blobs_labels", outline=False, outline_color="red", outline_alpha=1.0, contour_px=15
).pl.show()

def test_plot_can_control_label_infill(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_labels(
"blobs_labels",
Expand Down Expand Up @@ -921,3 +930,20 @@ def test_resolve_as_points_method_threshold_and_fallback():
# explicit matplotlib always matplotlib; empty always matplotlib
assert _resolve_as_points_method(rp_mpl, n=10**9, allow_datashader=True) == "matplotlib"
assert _resolve_as_points_method(rp_auto, n=0, allow_datashader=True) == "matplotlib"


# Regression tests for #748: the `outline` bool convenience toggle on render_labels.
def test_outline_toggle_true_draws_outline(sdata_blobs: SpatialData):
on = sdata_blobs.pl.render_labels("blobs_labels", outline=True, contour_px=15)
assert list(on.plotting_tree.values())[-1].outline_alpha == 1.0


def test_outline_toggle_none_preserves_default_off(sdata_blobs: SpatialData):
off = sdata_blobs.pl.render_labels("blobs_labels")
assert list(off.plotting_tree.values())[-1].outline_alpha == 0.0


def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
with pytest.warns(UserWarning, match="outline=False"):
out = sdata_blobs.pl.render_labels("blobs_labels", outline=False, outline_color="red")
assert list(out.plotting_tree.values())[-1].outline_alpha == 0.0
32 changes: 32 additions & 0 deletions tests/pl/test_render_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ def test_plot_can_render_polygons(self, sdata_blobs: SpatialData):
def test_plot_can_render_polygons_with_outline(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1).pl.show()

def test_plot_outline_toggle_true_draws_outline(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline=True).pl.show()

def test_plot_outline_toggle_false_suppresses_outline(self, sdata_blobs: SpatialData):
with pytest.warns(UserWarning, match="outline=False"):
sdata_blobs.pl.render_shapes(
element="blobs_polygons", outline=False, outline_color="red", outline_alpha=1
).pl.show()

def test_plot_can_render_polygons_with_str_colored_outline(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1, outline_color="red").pl.show()

Expand Down Expand Up @@ -2005,3 +2014,26 @@ def test_scale_geometries_matches_affinity_scale():
expected = np.array([affinity.scale(g, xfact=scale, yfact=scale) for g in geoms], dtype=object)
result = _scale_geometries(arr, scale)
assert all(shapely.equals_exact(a, b, tolerance=1e-9) for a, b in zip(expected, result, strict=True))


# Regression tests for #748: the `outline` bool convenience toggle on render_shapes.
def test_outline_toggle_true_draws_outline_like_alpha(sdata_blobs: SpatialData):
on = sdata_blobs.pl.render_shapes("blobs_polygons", outline=True)
explicit = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
on_alpha = list(on.plotting_tree.values())[-1].outline_alpha
explicit_alpha = list(explicit.plotting_tree.values())[-1].outline_alpha
assert on_alpha[0] > 0
assert on_alpha == explicit_alpha


def test_outline_toggle_none_preserves_inference(sdata_blobs: SpatialData):
off = sdata_blobs.pl.render_shapes("blobs_polygons")
inferred = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
assert list(off.plotting_tree.values())[-1].outline_alpha[0] == 0
assert list(inferred.plotting_tree.values())[-1].outline_alpha[0] > 0


def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
with pytest.warns(UserWarning, match="outline=False"):
out = sdata_blobs.pl.render_shapes("blobs_polygons", outline=False, outline_color="red")
assert list(out.plotting_tree.values())[-1].outline_alpha[0] == 0
Loading