Skip to content

Commit 076eeac

Browse files
authored
fix(shapes,labels): re-add outline bool toggle (#750)
1 parent 3d796b0 commit 076eeac

8 files changed

Lines changed: 97 additions & 0 deletions

src/spatialdata_plot/pl/_color.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import warnings
56
from collections.abc import Mapping, Sequence
67
from copy import copy
78
from dataclasses import dataclass, replace
@@ -312,6 +313,29 @@ def _set_outline(
312313
)
313314

314315

316+
def _resolve_outline_toggle(
317+
outline: bool | None,
318+
outline_alpha: float | int | tuple[float | int, float | int] | None,
319+
detail_params_set: bool,
320+
) -> float | int | tuple[float | int, float | int] | None:
321+
"""Map the tri-state ``outline`` toggle onto an effective ``outline_alpha``.
322+
323+
``None`` leaves it unchanged; ``True`` forces on (1.0 if unset/zero); ``False`` forces off and warns on conflict.
324+
"""
325+
if outline is None:
326+
return outline_alpha
327+
alpha_is_zero = outline_alpha is None or bool(np.all(np.asarray(outline_alpha, dtype=float) == 0.0))
328+
if outline:
329+
return 1.0 if alpha_is_zero else outline_alpha
330+
if detail_params_set or not alpha_is_zero:
331+
warnings.warn(
332+
"`outline=False` overrides the `outline_*` parameters you set; no outline will be drawn.",
333+
UserWarning,
334+
stacklevel=3,
335+
)
336+
return 0.0
337+
338+
315339
def _get_colors_for_categorical_obs(
316340
categories: Sequence[str | int],
317341
palette: ListedColormap | str | list[str] | None = None,

src/spatialdata_plot/pl/basic.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
_maybe_set_colors,
3434
_next_palette_colors,
3535
_prepare_cmap_norm,
36+
_resolve_outline_toggle,
3637
_set_outline,
3738
)
3839
from spatialdata_plot.pl._validate import (
@@ -324,6 +325,7 @@ def render_shapes(
324325
groups: list[str] | str | None = None,
325326
palette: dict[str, str] | list[str] | str | None = None,
326327
na_color: ColorLike | None = "default",
328+
outline: bool | None = None,
327329
outline_width: float | int | tuple[float | int, float | int] | None = None,
328330
outline_color: ColorLike | tuple[ColorLike] | None = None,
329331
outline_alpha: float | int | tuple[float | int, float | int] | None = None,
@@ -387,6 +389,10 @@ def render_shapes(
387389
elements are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
388390
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
389391
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
392+
outline : bool | None, default None
393+
Convenience on/off switch. ``None`` infers visibility from the ``outline_*`` params;
394+
``True`` forces an outline on (defaults fill in any unset width/color/alpha); ``False``
395+
forces it off, overriding and warning about any ``outline_*`` you set.
390396
outline_width : float | int | tuple[float | int, float | int], optional
391397
Width of the border. If 2 values are given (tuple), 2 borders are shown with these widths (outer & inner).
392398
If `outline_color` and/or `outline_alpha` are used to indicate that one/two outlines should be drawn, the
@@ -465,6 +471,9 @@ def render_shapes(
465471
"""
466472
if as_points:
467473
_validate_as_points_size(size)
474+
outline_alpha = _resolve_outline_toggle(
475+
outline, outline_alpha, outline_width is not None or outline_color is not None
476+
)
468477
panel_param_dicts = _expand_color_panels(
469478
self._sdata,
470479
color,
@@ -967,6 +976,7 @@ def render_labels(
967976
cmap: Colormap | str | None = None,
968977
norm: Normalize | None = None,
969978
na_color: ColorLike | None = "default",
979+
outline: bool | None = None,
970980
outline_alpha: float | int = 0.0,
971981
fill_alpha: float | int | None = None,
972982
outline_color: ColorLike | None = None,
@@ -1028,6 +1038,10 @@ def render_labels(
10281038
labels are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
10291039
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
10301040
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
1041+
outline : bool | None, default None
1042+
Convenience on/off switch; thickness is set by ``contour_px``. ``None`` infers visibility from
1043+
``outline_color``/``outline_alpha``; ``True`` forces on (alpha defaults to 1.0); ``False``
1044+
forces it off, overriding and warning about any params you set.
10311045
outline_alpha : float | int, default 0.0
10321046
Alpha value for the outline of the labels. Invisible by default.
10331047
fill_alpha : float | int | None, optional
@@ -1077,6 +1091,7 @@ def render_labels(
10771091
"""
10781092
if as_points:
10791093
_validate_as_points_size(size)
1094+
outline_alpha = cast("float | int", _resolve_outline_toggle(outline, outline_alpha, outline_color is not None))
10801095
panel_param_dicts = _expand_color_panels(
10811096
self._sdata,
10821097
color,
35.7 KB
Loading
51 KB
Loading
20.6 KB
Loading
27.9 KB
Loading

tests/pl/test_render_labels.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,15 @@ def test_plot_can_control_label_outline(self, sdata_blobs: SpatialData):
340340
contour_px=15,
341341
).pl.show()
342342

343+
def test_plot_outline_toggle_true_draws_outline(self, sdata_blobs: SpatialData):
344+
sdata_blobs.pl.render_labels("blobs_labels", outline=True, fill_alpha=0.0, contour_px=15).pl.show()
345+
346+
def test_plot_outline_toggle_false_suppresses_outline(self, sdata_blobs: SpatialData):
347+
with pytest.warns(UserWarning, match="outline=False"):
348+
sdata_blobs.pl.render_labels(
349+
"blobs_labels", outline=False, outline_color="red", outline_alpha=1.0, contour_px=15
350+
).pl.show()
351+
343352
def test_plot_can_control_label_infill(self, sdata_blobs: SpatialData):
344353
sdata_blobs.pl.render_labels(
345354
"blobs_labels",
@@ -921,3 +930,20 @@ def test_resolve_as_points_method_threshold_and_fallback():
921930
# explicit matplotlib always matplotlib; empty always matplotlib
922931
assert _resolve_as_points_method(rp_mpl, n=10**9, allow_datashader=True) == "matplotlib"
923932
assert _resolve_as_points_method(rp_auto, n=0, allow_datashader=True) == "matplotlib"
933+
934+
935+
# Regression tests for #748: the `outline` bool convenience toggle on render_labels.
936+
def test_outline_toggle_true_draws_outline(sdata_blobs: SpatialData):
937+
on = sdata_blobs.pl.render_labels("blobs_labels", outline=True, contour_px=15)
938+
assert list(on.plotting_tree.values())[-1].outline_alpha == 1.0
939+
940+
941+
def test_outline_toggle_none_preserves_default_off(sdata_blobs: SpatialData):
942+
off = sdata_blobs.pl.render_labels("blobs_labels")
943+
assert list(off.plotting_tree.values())[-1].outline_alpha == 0.0
944+
945+
946+
def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
947+
with pytest.warns(UserWarning, match="outline=False"):
948+
out = sdata_blobs.pl.render_labels("blobs_labels", outline=False, outline_color="red")
949+
assert list(out.plotting_tree.values())[-1].outline_alpha == 0.0

tests/pl/test_render_shapes.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ def test_plot_can_render_polygons(self, sdata_blobs: SpatialData):
108108
def test_plot_can_render_polygons_with_outline(self, sdata_blobs: SpatialData):
109109
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1).pl.show()
110110

111+
def test_plot_outline_toggle_true_draws_outline(self, sdata_blobs: SpatialData):
112+
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline=True).pl.show()
113+
114+
def test_plot_outline_toggle_false_suppresses_outline(self, sdata_blobs: SpatialData):
115+
with pytest.warns(UserWarning, match="outline=False"):
116+
sdata_blobs.pl.render_shapes(
117+
element="blobs_polygons", outline=False, outline_color="red", outline_alpha=1
118+
).pl.show()
119+
111120
def test_plot_can_render_polygons_with_str_colored_outline(self, sdata_blobs: SpatialData):
112121
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline_alpha=1, outline_color="red").pl.show()
113122

@@ -2005,3 +2014,26 @@ def test_scale_geometries_matches_affinity_scale():
20052014
expected = np.array([affinity.scale(g, xfact=scale, yfact=scale) for g in geoms], dtype=object)
20062015
result = _scale_geometries(arr, scale)
20072016
assert all(shapely.equals_exact(a, b, tolerance=1e-9) for a, b in zip(expected, result, strict=True))
2017+
2018+
2019+
# Regression tests for #748: the `outline` bool convenience toggle on render_shapes.
2020+
def test_outline_toggle_true_draws_outline_like_alpha(sdata_blobs: SpatialData):
2021+
on = sdata_blobs.pl.render_shapes("blobs_polygons", outline=True)
2022+
explicit = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
2023+
on_alpha = list(on.plotting_tree.values())[-1].outline_alpha
2024+
explicit_alpha = list(explicit.plotting_tree.values())[-1].outline_alpha
2025+
assert on_alpha[0] > 0
2026+
assert on_alpha == explicit_alpha
2027+
2028+
2029+
def test_outline_toggle_none_preserves_inference(sdata_blobs: SpatialData):
2030+
off = sdata_blobs.pl.render_shapes("blobs_polygons")
2031+
inferred = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
2032+
assert list(off.plotting_tree.values())[-1].outline_alpha[0] == 0
2033+
assert list(inferred.plotting_tree.values())[-1].outline_alpha[0] > 0
2034+
2035+
2036+
def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
2037+
with pytest.warns(UserWarning, match="outline=False"):
2038+
out = sdata_blobs.pl.render_shapes("blobs_polygons", outline=False, outline_color="red")
2039+
assert list(out.plotting_tree.values())[-1].outline_alpha[0] == 0

0 commit comments

Comments
 (0)