Skip to content

Commit 1254107

Browse files
author
Sonja Stockhaus
committed
update color dataclass
1 parent 7b86cc9 commit 1254107

1 file changed

Lines changed: 70 additions & 34 deletions

File tree

src/spatialdata_plot/pl/utils.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections import OrderedDict
66
from collections.abc import Iterable, Mapping, Sequence
77
from copy import copy
8+
from dataclasses import dataclass
89
from functools import partial
910
from pathlib import Path
1011
from types import MappingProxyType
@@ -2551,59 +2552,94 @@ def _convert_alpha_to_datashader_range(alpha: float) -> float:
25512552
return min([254, alpha * 255])
25522553

25532554

2554-
class ColorHandler:
2555-
"""Validate, parse and store a single color."""
2555+
@dataclass
2556+
class Color:
2557+
"""Validate, parse and store a single color.
25562558
2557-
color_hex: str | None
2559+
Accepts a color and an alpha value.
2560+
If no color or "default" is given, the default color "lightgray" is used.
2561+
If no alpha is given, the default of completely opaque is used ("FF").
2562+
At all times, if color indicates an alpha value, for instance as part of a hex string, the alpha parameter takes
2563+
precedence if given.
2564+
"""
2565+
2566+
color: str
2567+
alpha: str
25582568

2559-
def __init__(self, color: ColorLike | list[float] | None):
2569+
def __post__init__(self) -> None:
25602570
"""Validate input and store as hex color.
25612571
2562-
`color` can be
2572+
`self.color` can be
25632573
- str: hex representation, named color or "default"
25642574
- None (values won't be shown)
25652575
- tuple[float, ...] | list[float]: RGB(A) array (all values within [0, 1]!)
25662576
"""
2567-
if color is None:
2568-
self.color_hex = None
2569-
elif isinstance(color, str):
2577+
# 1) Validate alpha value
2578+
if isinstance(self.alpha, float | int):
2579+
if self.alpha <= 1.0 and self.alpha >= 0.0:
2580+
# Convert float alpha to hex representation
2581+
self.alpha = int(np.round(self.alpha * 255))
2582+
self.alpha = hex(self.alpha)[2:].upper()
2583+
if len(self.alpha) == 1:
2584+
self.alpha = "0" + self.alpha
2585+
else:
2586+
raise ValueError(f"Invalid alpha value `{self.alpha}`, must lie within [0.0, 1.0].")
2587+
elif self.alpha is not None:
2588+
raise ValueError(f"Invalid alpha value `{self.alpha}`, must be None or a float | int within [0.0, 1.0].")
2589+
2590+
# 2) Validate color value
2591+
if self.color is None or self.color == "default":
2592+
self.color = colors.to_hex("lightgray", keep_alpha=False)
2593+
elif isinstance(self.color, str):
25702594
# already hex
2571-
if color.startswith("#"):
2572-
if len(color) not in [7, 9]:
2573-
raise ValueError("Invalid hex color length: must be either '#RRGGBB' or '#RRGGBBAA'")
2574-
self.color_hex = color.lower()
2575-
if len(self.color_hex) == 7:
2576-
self.color_hex += "ff" # add alpha of 1.0 if needed
2577-
if not all(c in "0123456789abcdef" for c in self.color_hex[1:]):
2595+
if self.color.startswith("#"):
2596+
if len(self.color) not in [7, 9]:
2597+
raise ValueError("Invalid hex color length: only formats '#RRGGBB' and '#RRGGBBAA' are supported.")
2598+
self.color = self.color.upper()
2599+
if not all(c in "0123456789ABCDEF" for c in self.color[1:]):
25782600
raise ValueError("Invalid hex color: contains non-hex characters")
2579-
# "default" should refer to lightgray
2580-
elif color == "default":
2581-
self.color_hex = to_hex("lightgray", keep_alpha=True)
2601+
if len(self.color) == 9:
2602+
if self.alpha is None:
2603+
self.alpha = self.color[7:]
2604+
self.color = self.color[:7]
25822605
# named color
25832606
else:
25842607
# matplotlib raises ValueError in case of invalid color name
2585-
self.color_hex = to_hex(color, keep_alpha=True)
2586-
elif isinstance(color, list[float] | tuple[float, ...]):
2587-
if len(color) < 3:
2588-
raise ValueError(f"Color `{color}` can't be interpreted as RGB(A) array, needs at least 3 values.")
2608+
self.color = colors.to_hex(self.color, keep_alpha=False)
2609+
elif isinstance(self.color, list[float] | tuple[float, ...]):
2610+
if len(self.color) < 3:
2611+
raise ValueError(f"Color `{self.color}` can't be interpreted as RGB(A) array, needs at least 3 values.")
25892612
# get first 3-4 values
2590-
r, g, b = color[0], color[1], color[2]
2591-
a = 1.0 if len(color) == 3 else color[3]
2613+
r, g, b = self.color[0], self.color[1], self.color[2]
2614+
a = 1.0 if len(self.color) == 3 else self.color[3]
25922615
if any(np.array([r, g, b, a]) > 1) or any(np.array([r, g, b, a]) < 0):
2593-
raise ValueError(f"Invalid color `{color}`, all values in RGB(A) array must be within [0.0, 1.0].")
2594-
self.color_hex = colors.rgb2hex((r, g, b, a), keep_alpha=True)
2616+
raise ValueError(f"Invalid color `{self.color}`, all values in RGB(A) array must be within [0.0, 1.0].")
2617+
self.color = colors.rgb2hex((r, g, b, a), keep_alpha=False)
2618+
if self.alpha is None:
2619+
self.alpha = colors.rgb2hex((r, g, b, a), keep_alpha=True)[7:]
2620+
elif isinstance(self.color, float | int):
2621+
raise TypeError(
2622+
f"Invalid type `{type(self.color)}` for a color, expecting str | None | tuple[float, ...] | "
2623+
"list[float]. Note that unlike in matplotlib, giving a float within [0, 1] as a greyscale value is not "
2624+
"supported here!"
2625+
)
25952626
else:
25962627
raise TypeError(
2597-
f"Invalid type `{type(color)}` for a color, expecting str | None | tuple[float, ...] | list[float]."
2628+
f"Invalid type `{type(self.color)}` for color, expecting str | None | tuple[float, ...] | list[float]."
25982629
)
25992630

2600-
def get_hex(self) -> str | None:
2631+
# 3) Set default alpha if still necessary
2632+
if self.alpha is None:
2633+
self.alpha = "FF" # default: completely opaque
2634+
2635+
def get_hex_with_alpha(self) -> str:
26012636
"""Get color value as '#RRGGBBAA'."""
2602-
return self.color_hex
2637+
return self.color + self.alpha
26032638

2604-
def get_hex_without_alpha(self) -> str | None:
2639+
def get_hex(self) -> str:
26052640
"""Get color value as '#RRGGBB'."""
2606-
if isinstance(self.color_hex, str):
2607-
# return hex after stripping the last 2 positions (=alpha)
2608-
return self.color_hex[0:7]
2609-
return self.color_hex
2641+
return self.color
2642+
2643+
def get_alpha_as_float(self) -> float:
2644+
"""Return alpha as value within [0.0, 1.0]."""
2645+
return int(self.alpha, 16) / 255

0 commit comments

Comments
 (0)