Skip to content

Commit da53b58

Browse files
committed
fix(shapes,labels): re-add outline bool toggle (#748)
The `outline` parameter was removed in favor of inferring outline visibility from outline_alpha/outline_width/outline_color. It kept working silently only because **kwargs swallowed it; removing **kwargs turned every outline=... call into a TypeError, breaking notebooks and downstream code. Re-introduce `outline` as a first-class convenience toggle (tri-state): - None (default): infer visibility from the outline_* detail params (no change). - True: force outline on, defaulting any unset detail param. - False: force outline off, overriding detail params (warns on conflict). outline is authoritative over inference; detail params only control how the outline looks. Applied to both render_shapes and render_labels.
1 parent 227434d commit da53b58

4 files changed

Lines changed: 88 additions & 0 deletions

File tree

src/spatialdata_plot/pl/_color.py

Lines changed: 26 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,31 @@ 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+
"""Resolve the ``outline`` bool toggle onto an effective ``outline_alpha``.
322+
323+
- ``None`` -> ``outline_alpha`` unchanged (visibility inferred from the ``outline_*`` params).
324+
- ``True`` -> force on: 1.0 if ``outline_alpha`` is unset/zero, else the given value.
325+
- ``False`` -> force off: 0.0, warning if any ``outline_*`` param was also set.
326+
"""
327+
if outline is None:
328+
return outline_alpha
329+
alpha_is_zero = outline_alpha is None or bool(np.all(np.asarray(outline_alpha, dtype=float) == 0.0))
330+
if outline:
331+
return 1.0 if alpha_is_zero else outline_alpha
332+
if detail_params_set or not alpha_is_zero:
333+
warnings.warn(
334+
"`outline=False` overrides the `outline_*` parameters you set; no outline will be drawn.",
335+
UserWarning,
336+
stacklevel=3,
337+
)
338+
return 0.0
339+
340+
315341
def _get_colors_for_categorical_obs(
316342
categories: Sequence[str | int],
317343
palette: ListedColormap | str | list[str] | None = None,

src/spatialdata_plot/pl/basic.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
_maybe_set_colors,
3535
_next_palette_colors,
3636
_prepare_cmap_norm,
37+
_resolve_outline_toggle,
3738
_set_outline,
3839
)
3940
from spatialdata_plot.pl._validate import (
@@ -325,6 +326,7 @@ def render_shapes(
325326
groups: list[str] | str | None = None,
326327
palette: dict[str, str] | list[str] | str | None = None,
327328
na_color: ColorLike | None = "default",
329+
outline: bool | None = None,
328330
outline_width: float | int | tuple[float | int, float | int] | None = None,
329331
outline_color: ColorLike | tuple[ColorLike] | None = None,
330332
outline_alpha: float | int | tuple[float | int, float | int] | None = None,
@@ -388,6 +390,10 @@ def render_shapes(
388390
elements are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
389391
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
390392
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
393+
outline : bool | None, default None
394+
Convenience on/off switch. ``None`` infers visibility from the ``outline_*`` params;
395+
``True`` forces an outline on (defaults fill in any unset width/color/alpha); ``False``
396+
forces it off, overriding and warning about any ``outline_*`` you set.
391397
outline_width : float | int | tuple[float | int, float | int], optional
392398
Width of the border. If 2 values are given (tuple), 2 borders are shown with these widths (outer & inner).
393399
If `outline_color` and/or `outline_alpha` are used to indicate that one/two outlines should be drawn, the
@@ -466,6 +472,9 @@ def render_shapes(
466472
"""
467473
if as_points:
468474
_validate_as_points_size(size)
475+
outline_alpha = _resolve_outline_toggle(
476+
outline, outline_alpha, outline_width is not None or outline_color is not None
477+
)
469478
panel_param_dicts = _expand_color_panels(
470479
self._sdata,
471480
color,
@@ -968,6 +977,7 @@ def render_labels(
968977
cmap: Colormap | str | None = None,
969978
norm: Normalize | None = None,
970979
na_color: ColorLike | None = "default",
980+
outline: bool | None = None,
971981
outline_alpha: float | int = 0.0,
972982
fill_alpha: float | int | None = None,
973983
outline_color: ColorLike | None = None,
@@ -1029,6 +1039,10 @@ def render_labels(
10291039
labels are hidden. Pass any explicit color (e.g. ``"lightgray"``) to show them in that color instead.
10301040
Accepts a named color (``"red"``), a hex string (``"#000000ff"``), or an RGB/RGBA list
10311041
(``[1.0, 0.0, 0.0, 1.0]``). Pass ``None`` to make NA values fully transparent.
1042+
outline : bool | None, default None
1043+
Convenience on/off switch; thickness is set by ``contour_px``. ``None`` infers visibility from
1044+
``outline_color``/``outline_alpha``; ``True`` forces on (alpha defaults to 1.0); ``False``
1045+
forces it off, overriding and warning about any params you set.
10321046
outline_alpha : float | int, default 0.0
10331047
Alpha value for the outline of the labels. Invisible by default.
10341048
fill_alpha : float | int | None, optional
@@ -1078,6 +1092,8 @@ def render_labels(
10781092
"""
10791093
if as_points:
10801094
_validate_as_points_size(size)
1095+
# labels outline_alpha is always scalar; cast narrows the helper's wider return union
1096+
outline_alpha = cast("float | int", _resolve_outline_toggle(outline, outline_alpha, outline_color is not None))
10811097
panel_param_dicts = _expand_color_panels(
10821098
self._sdata,
10831099
color,

tests/pl/test_render_labels.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,3 +921,21 @@ def test_resolve_as_points_method_threshold_and_fallback():
921921
# explicit matplotlib always matplotlib; empty always matplotlib
922922
assert _resolve_as_points_method(rp_mpl, n=10**9, allow_datashader=True) == "matplotlib"
923923
assert _resolve_as_points_method(rp_auto, n=0, allow_datashader=True) == "matplotlib"
924+
925+
926+
# Regression tests for #748: the `outline` bool convenience toggle on render_labels.
927+
def test_outline_toggle_true_draws_outline(sdata_blobs: SpatialData):
928+
# outline=True must not raise (the reported regression) and forces outline_alpha to 1.0.
929+
on = sdata_blobs.pl.render_labels("blobs_labels", outline=True, contour_px=15)
930+
assert list(on.plotting_tree.values())[-1].outline_alpha == 1.0
931+
932+
933+
def test_outline_toggle_none_preserves_default_off(sdata_blobs: SpatialData):
934+
off = sdata_blobs.pl.render_labels("blobs_labels")
935+
assert list(off.plotting_tree.values())[-1].outline_alpha == 0.0
936+
937+
938+
def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
939+
with pytest.warns(UserWarning, match="outline=False"):
940+
out = sdata_blobs.pl.render_labels("blobs_labels", outline=False, outline_color="red")
941+
assert list(out.plotting_tree.values())[-1].outline_alpha == 0.0

tests/pl/test_render_shapes.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,3 +2005,31 @@ def test_scale_geometries_matches_affinity_scale():
20052005
expected = np.array([affinity.scale(g, xfact=scale, yfact=scale) for g in geoms], dtype=object)
20062006
result = _scale_geometries(arr, scale)
20072007
assert all(shapely.equals_exact(a, b, tolerance=1e-9) for a, b in zip(expected, result, strict=True))
2008+
2009+
2010+
# Regression tests for #748: the `outline` bool convenience toggle on render_shapes.
2011+
# It resolves onto the outline_* detail params; outline is authoritative over inference.
2012+
def test_outline_toggle_true_draws_outline_like_alpha(sdata_blobs: SpatialData):
2013+
# outline=True must not raise (the reported regression) and must draw an outline
2014+
# equivalent to setting outline_alpha=1.0 explicitly.
2015+
on = sdata_blobs.pl.render_shapes("blobs_polygons", outline=True)
2016+
explicit = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
2017+
on_alpha = list(on.plotting_tree.values())[-1].outline_alpha
2018+
explicit_alpha = list(explicit.plotting_tree.values())[-1].outline_alpha
2019+
assert on_alpha[0] > 0
2020+
assert on_alpha == explicit_alpha
2021+
2022+
2023+
def test_outline_toggle_none_preserves_inference(sdata_blobs: SpatialData):
2024+
# Default (outline=None) keeps current behavior: no outline unless a detail param is set.
2025+
off = sdata_blobs.pl.render_shapes("blobs_polygons")
2026+
inferred = sdata_blobs.pl.render_shapes("blobs_polygons", outline_alpha=1.0)
2027+
assert list(off.plotting_tree.values())[-1].outline_alpha[0] == 0
2028+
assert list(inferred.plotting_tree.values())[-1].outline_alpha[0] > 0
2029+
2030+
2031+
def test_outline_toggle_false_forces_off_and_warns(sdata_blobs: SpatialData):
2032+
# outline=False wins over explicit detail params and warns about the conflict.
2033+
with pytest.warns(UserWarning, match="outline=False"):
2034+
out = sdata_blobs.pl.render_shapes("blobs_polygons", outline=False, outline_color="red")
2035+
assert list(out.plotting_tree.values())[-1].outline_alpha[0] == 0

0 commit comments

Comments
 (0)