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
38 changes: 32 additions & 6 deletions src/spatialdata_plot/pl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,24 +209,50 @@ def _get_coordinate_system_mapping(sdata: SpatialData) -> dict[str, list[str]]:
return mapping


_MPL_SINGLE_LETTER_COLORS = frozenset("bgrcmykw")


def _is_color_like(color: Any) -> bool:
"""Check if a value is a valid color.

For discussion, see: https://github.com/scverse/spatialdata-plot/issues/327.
matplotlib accepts strings in [0, 1] as grey-scale values - therefore,
"0" and "1" are considered valid colors. However, we won't do that
so we're filtering these out.
We reject several matplotlib shorthand notations that are likely to collide
with column or gene names. For discussion, see:

- https://github.com/scverse/spatialdata-plot/issues/211
- https://github.com/scverse/spatialdata-plot/issues/327

Rejected shorthands:

- Greyscale strings: ``"0"``, ``"0.5"``, ``"1"`` (floats in [0, 1])
- Short hex: ``"#RGB"`` / ``"#RGBA"`` (only ``#RRGGBB`` / ``#RRGGBBAA`` accepted)
- Single-letter colors: ``"b"``, ``"g"``, ``"r"``, ``"c"``, ``"m"``, ``"y"``, ``"k"``, ``"w"``
- CN cycle notation: ``"C0"``, ``"C1"``, …
- ``tab:`` prefixed colors: ``"tab:blue"``, ``"tab:orange"``, …
- ``xkcd:`` prefixed colors: ``"xkcd:sky blue"``, …
"""
if isinstance(color, str):
# greyscale strings
try:
num_value = float(color)
if 0 <= num_value <= 1:
return False
except ValueError:
# we're not dealing with what matplotlib considers greyscale
pass

# short hex
if color.startswith("#") and len(color) not in [7, 9]:
# we only accept hex colors in the form #RRGGBB or #RRGGBBAA, not short forms as matplotlib does
return False

# single-letter color shortcuts
if color in _MPL_SINGLE_LETTER_COLORS:
return False

# CN cycle notation (C0, C1, …)
if len(color) >= 2 and color[0] == "C" and color[1:].isdigit():
return False

# tab: and xkcd: prefixed colors
if color.startswith(("tab:", "xkcd:")):
return False

return bool(colors.is_color_like(color))
Expand Down
27 changes: 27 additions & 0 deletions tests/pl/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,38 @@ def test_plot_transparent_cmap_shapes_clip_false(self, sdata_blobs: SpatialData)
@pytest.mark.parametrize(
"color_result",
[
# greyscale strings rejected
("0", False),
("0.5", False),
("1", False),
# valid full-form colors accepted
("#00ff00", True),
("#00ff00aa", True),
((0.0, 1.0, 0.0, 1.0), True),
("red", True),
("blue", True),
# short hex rejected
("#f00", False),
("#f00a", False),
# single-letter shortcuts rejected (#211)
("b", False),
("g", False),
("r", False),
("c", False),
("m", False),
("y", False),
("k", False),
("w", False),
# CN cycle notation rejected (#211)
("C0", False),
("C1", False),
("C10", False),
# tab: prefixed rejected (#211)
("tab:blue", False),
("tab:orange", False),
# xkcd: prefixed rejected (#211)
("xkcd:sky blue", False),
("xkcd:red", False),
],
)
def test_is_color_like(color_result: tuple[ColorLike, bool]):
Expand Down
Loading