Skip to content

Commit bd285f6

Browse files
authored
String column names in [0, 1] are now no longer interpreted as colors (#331)
1 parent e97a7fc commit bd285f6

File tree

4 files changed

+73
-11
lines changed

4 files changed

+73
-11
lines changed

src/spatialdata_plot/pl/utils.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@
8383
ColorLike = Union[tuple[float, ...], str]
8484

8585

86+
def _is_color_like(color: Any) -> bool:
87+
"""Check if a value is a valid color, returns False for pseudo-bools.
88+
89+
For discussion, see: https://github.com/scverse/spatialdata-plot/issues/327.
90+
matplotlib accepts strings in [0, 1] as grey-scale values - therefore,
91+
"0" and "1" are considered valid colors. However, we won't do that
92+
so we're filtering these out.
93+
"""
94+
if isinstance(color, bool):
95+
return False
96+
if isinstance(color, str):
97+
try:
98+
num_value = float(color)
99+
if 0 <= num_value <= 1:
100+
return False
101+
except ValueError:
102+
# we're not dealing with what matplotlib considers greyscale
103+
pass
104+
105+
return bool(colors.is_color_like(color))
106+
107+
86108
def _prepare_params_plot(
87109
# this param is inferred when `pl.show`` is called
88110
num_panels: int,
@@ -532,7 +554,15 @@ def _normalize(
532554

533555
perc = np.percentile(img, [pmin, pmax])
534556

535-
norm = (img - perc[0]) / (perc[1] - perc[0] + eps)
557+
# Ensure perc is an array of two elements
558+
if np.isscalar(perc):
559+
logger.warning(
560+
"Percentile range is too small, using the same percentile for both min "
561+
"and max. Consider using a larger percentile range."
562+
)
563+
perc = np.array([perc, perc])
564+
565+
norm = (img - perc[0]) / (perc[1] - perc[0] + eps) # type: ignore
536566

537567
if clip:
538568
norm = np.clip(norm, 0, 1)
@@ -727,7 +757,7 @@ def _map_color_seg(
727757
val_im = np.squeeze(val_im, axis=0)
728758
if "#" in str(color_vector[0]):
729759
# we have hex colors
730-
assert all(colors.is_color_like(c) for c in color_vector), "Not all values are color-like."
760+
assert all(_is_color_like(c) for c in color_vector), "Not all values are color-like."
731761
cols = colors.to_rgba_array(color_vector)
732762
else:
733763
cols = cmap_params.cmap(cmap_params.norm(color_vector))
@@ -1557,15 +1587,11 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
15571587
if (contour_px := param_dict.get("contour_px")) and not isinstance(contour_px, int):
15581588
raise TypeError("Parameter 'contour_px' must be an integer.")
15591589

1560-
if (color := param_dict.get("color")) and element_type in {
1561-
"shapes",
1562-
"points",
1563-
"labels",
1564-
}:
1590+
if (color := param_dict.get("color")) and element_type in {"shapes", "points", "labels"}:
15651591
if not isinstance(color, str):
15661592
raise TypeError("Parameter 'color' must be a string.")
15671593
if element_type in {"shapes", "points"}:
1568-
if colors.is_color_like(color):
1594+
if _is_color_like(color):
15691595
logger.info("Value for parameter 'color' appears to be a color, using it as such.")
15701596
param_dict["col_for_color"] = None
15711597
else:
@@ -1645,7 +1671,7 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
16451671
raise TypeError("Parameter 'cmap' must be a string, a Colormap, or a list of these types.")
16461672

16471673
if (na_color := param_dict.get("na_color")) != "default" and (
1648-
na_color is not None and not colors.is_color_like(na_color)
1674+
na_color is not None and not _is_color_like(na_color)
16491675
):
16501676
raise ValueError("Parameter 'na_color' must be color-like.")
16511677

21 KB
Loading

tests/pl/test_render_shapes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import spatialdata_plot # noqa: F401
88
from anndata import AnnData
99
from shapely.geometry import MultiPolygon, Point, Polygon
10-
from spatialdata import SpatialData
10+
from spatialdata import SpatialData, deepcopy
1111
from spatialdata.models import ShapesModel, TableModel
1212

1313
from tests.conftest import DPI, PlotTester, PlotTesterMeta
@@ -94,7 +94,7 @@ def _make_multi():
9494
sdata.pl.render_shapes(color="val", outline=True, fill_alpha=0.3).pl.show()
9595

9696
def test_plot_can_color_from_geodataframe(self, sdata_blobs: SpatialData):
97-
blob = sdata_blobs
97+
blob = deepcopy(sdata_blobs)
9898
blob["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
9999
blob["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
100100
blob.shapes["blobs_polygons"]["value"] = [1, 10, 1, 20, 1]

tests/pl/test_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Union
2+
13
import matplotlib
24
import matplotlib.pyplot as plt
35
import numpy as np
@@ -23,6 +25,11 @@
2325
# the comp. function can be accessed as `self.compare(<your_filename>, tolerance=<your_tolerance>)`
2426
# ".png" is appended to <your_filename>, no need to set it
2527

28+
# replace with
29+
# from spatialdata._types import ColorLike
30+
# once https://github.com/scverse/spatialdata/pull/689/ is in a release
31+
ColorLike = Union[tuple[float, ...], str]
32+
2633

2734
class TestUtils(PlotTester, metaclass=PlotTesterMeta):
2835
@pytest.mark.parametrize(
@@ -35,6 +42,18 @@ class TestUtils(PlotTester, metaclass=PlotTesterMeta):
3542
def test_plot_set_outline_accepts_str_or_float_or_list_thereof(self, sdata_blobs: SpatialData, outline_color):
3643
sdata_blobs.pl.render_shapes(element="blobs_polygons", outline=True, outline_color=outline_color).pl.show()
3744

45+
@pytest.mark.parametrize(
46+
"colname",
47+
["0", "0.5", "1"],
48+
)
49+
def test_plot_colnames_that_are_valid_matplotlib_greyscale_colors_are_not_evaluated_as_colors(
50+
self, sdata_blobs: SpatialData, colname: str
51+
):
52+
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
53+
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
54+
sdata_blobs.shapes["blobs_polygons"][colname] = [1, 2, 3, 5, 20]
55+
sdata_blobs.pl.render_shapes("blobs_polygons", color=colname).pl.show()
56+
3857
def test_plot_can_set_zero_in_cmap_to_transparent(self, sdata_blobs: SpatialData):
3958
from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent
4059

@@ -60,6 +79,23 @@ def test_plot_can_set_zero_in_cmap_to_transparent(self, sdata_blobs: SpatialData
6079
).pl.show(ax=axs[1], colorbar=False)
6180

6281

82+
@pytest.mark.parametrize(
83+
"color_result",
84+
[
85+
("0", False),
86+
("0.5", False),
87+
("1", False),
88+
("#00ff00", True),
89+
((0.0, 1.0, 0.0, 1.0), True),
90+
],
91+
)
92+
def test_is_color_like(color_result: tuple[ColorLike, bool]):
93+
94+
color, result = color_result
95+
96+
assert spatialdata_plot.pl.utils._is_color_like(color) == result
97+
98+
6399
@pytest.mark.parametrize(
64100
"input_output",
65101
[

0 commit comments

Comments
 (0)