-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathrender_params.py
More file actions
301 lines (253 loc) · 10.3 KB
/
render_params.py
File metadata and controls
301 lines (253 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Literal
import numpy as np
from matplotlib.axes import Axes
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Colormap, ListedColormap, Normalize, rgb2hex, to_hex
from matplotlib.figure import Figure
_FontWeight = Literal["light", "normal", "medium", "semibold", "bold", "heavy", "black"]
_FontSize = Literal["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"]
# replace with
# from spatialdata._types import ColorLike
# once https://github.com/scverse/spatialdata/pull/689/ is in a release
ColorLike = tuple[float, ...] | str
# NOTE: defined here instead of utils to avoid circular import
@dataclass(kw_only=True)
class Color:
"""Validate, parse and store a single color.
Accepts a color and an alpha value.
If no color or "default" is given, the default color "lightgray" is used.
If no alpha is given, the default of completely opaque is used ("ff").
At all times, if color indicates an alpha value, for instance as part of a hex string, the alpha parameter takes
precedence if given.
"""
color: str
alpha: str
default_color_set: bool = False
user_defined_alpha: bool = False
def __init__(
self, color: None | str | list[float] | tuple[float, ...] = "default", alpha: float | int | None = None
) -> None:
# 1) Validate alpha value
if alpha is None:
self.alpha = "ff" # default: completely opaque
elif isinstance(alpha, float | int):
if alpha <= 1.0 and alpha >= 0.0:
# Convert float alpha to hex representation
self.alpha = hex(int(np.round(alpha * 255)))[2:].lower()
if len(self.alpha) == 1:
self.alpha = "0" + self.alpha
self.user_defined_alpha = True
else:
raise ValueError(f"Invalid alpha value `{alpha}`, must lie within [0.0, 1.0].")
else:
raise ValueError(f"Invalid alpha value `{alpha}`, must be None or a float | int within [0.0, 1.0].")
# 2) Validate color value
if color is None:
self.color = to_hex("lightgray", keep_alpha=False)
# setting color to None should lead to full transparency (except alpha is set manually)
if alpha is None:
self.alpha = "00"
elif color == "default":
self.default_color_set = True
self.color = to_hex("lightgray", keep_alpha=False)
elif isinstance(color, str):
# already hex
if color.startswith("#"):
if len(color) not in [7, 9]:
raise ValueError("Invalid hex color length: only formats '#RRGGBB' and '#RRGGBBAA' are supported.")
self.color = color.lower()
if not all(c in "0123456789abcdef" for c in self.color[1:]):
raise ValueError("Invalid hex color: contains non-hex characters")
if len(self.color) == 9:
if alpha is None:
self.alpha = self.color[7:]
self.user_defined_alpha = True
self.color = self.color[:7]
else:
try:
float(color)
except ValueError:
# we're not dealing with what matplotlib considers greyscale
pass
else:
raise TypeError(
f"Invalid type `{type(color)}` for a color, expecting str | None | tuple[float, ...] | "
"list[float]. Note that unlike in matplotlib, giving a string of a number within [0, 1] as a "
"greyscale value is not supported here!"
)
# matplotlib raises ValueError in case of invalid color name
self.color = to_hex(color, keep_alpha=False)
elif isinstance(color, list | tuple):
if len(color) < 3:
raise ValueError(f"Color `{color}` can't be interpreted as RGB(A) array, needs 3 or 4 values!")
if len(color) > 4:
raise ValueError(f"Color `{color}` can't be interpreted as RGB(A) array, needs 3 or 4 values!")
# get first 3-4 values
r, g, b = color[0], color[1], color[2]
a = 1.0
if len(color) == 4:
a = color[3]
self.user_defined_alpha = True
if (
not isinstance(r, int | float)
or not isinstance(g, int | float)
or not isinstance(b, int | float)
or not isinstance(a, int | float)
):
raise ValueError(f"Invalid color `{color}`, all values in RGB(A) array must be int or float.")
if any(np.array([r, g, b, a]) > 1) or any(np.array([r, g, b, a]) < 0):
raise ValueError(f"Invalid color `{color}`, all values in RGB(A) array must be within [0.0, 1.0].")
self.color = rgb2hex((r, g, b, a), keep_alpha=False)
if alpha is None:
self.alpha = rgb2hex((r, g, b, a), keep_alpha=True)[7:]
else:
raise TypeError(
f"Invalid type `{type(color)}` for color, expecting str | None | tuple[float, ...] | list[float]."
)
def get_hex_with_alpha(self) -> str:
"""Get color value as '#RRGGBBAA'."""
return self.color + self.alpha
def get_hex(self) -> str:
"""Get color value as '#RRGGBB'."""
return self.color
def get_alpha_as_float(self) -> float:
"""Return alpha as value within [0.0, 1.0]."""
return int(self.alpha, 16) / 255
def color_modified_by_user(self) -> bool:
"""Get whether a color was passed when the object was created."""
return not self.default_color_set
def alpha_is_user_defined(self) -> bool:
"""Get whether an alpha was set during object creation."""
return self.user_defined_alpha
def is_fully_transparent(self) -> bool:
"""Check whether this color is fully transparent (alpha == 0)."""
return self.alpha == "00"
@dataclass
class CmapParams:
"""Cmap params."""
cmap: Colormap
norm: Normalize
na_color: Color
cmap_is_default: bool = True
@dataclass
class FigParams:
"""Figure params."""
fig: Figure
ax: Axes
num_panels: int
axs: Sequence[Axes] | None = None
title: str | Sequence[str] | None = None
ax_labels: Sequence[str] | None = None
frameon: bool | None = None
@dataclass
class OutlineParams:
"""Cmap params."""
outer_outline_color: Color | None = None
outer_outline_linewidth: float = 1.5
inner_outline_color: Color | None = None
inner_outline_linewidth: float = 0.5
@dataclass
class LegendParams:
"""Legend params."""
legend_fontsize: int | float | _FontSize | None = None
legend_fontweight: int | _FontWeight = "bold"
legend_loc: str | None = "right margin"
legend_fontoutline: int | None = None
na_in_legend: bool = True
colorbar: bool = True
@dataclass
class ColorbarSpec:
"""Data required to create a colorbar."""
ax: Axes
mappable: ScalarMappable
params: dict[str, object] | None = None
label: str | None = None
alpha: float | None = None
CBAR_DEFAULT_LOCATION = "right"
CBAR_DEFAULT_FRACTION = 0.075
CBAR_DEFAULT_PAD = 0.015
@dataclass
class ScalebarParams:
"""Scalebar params."""
scalebar_dx: Sequence[float] | None = None
scalebar_units: Sequence[str] | None = None
@dataclass
class ShapesRenderParams:
"""Shapes render parameters.."""
cmap_params: CmapParams
outline_params: OutlineParams
element: str
color: Color | None = None
col_for_color: str | None = None
groups: str | list[str] | None = None
contour_px: int | None = None
palette: ListedColormap | list[str] | None = None
outline_alpha: tuple[float, float] = (1.0, 1.0)
fill_alpha: float = 0.3
scale: float = 1.0
transfunc: Callable[[float], float] | None = None
method: str | None = None
zorder: int = 0
table_name: str | None = None
table_layer: str | None = None
shape: Literal["circle", "hex", "visium_hex", "square"] | None = None
ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None
colorbar: bool | str | None = "auto"
colorbar_params: dict[str, object] | None = None
@dataclass
class PointsRenderParams:
"""Points render parameters.."""
cmap_params: CmapParams
element: str
color: Color | None = None
col_for_color: str | None = None
groups: str | list[str] | None = None
palette: ListedColormap | list[str] | None = None
alpha: float = 1.0
size: float = 1.0
transfunc: Callable[[float], float] | None = None
method: str | None = None
zorder: int = 0
table_name: str | None = None
table_layer: str | None = None
ds_reduction: Literal["sum", "mean", "any", "count", "std", "var", "max", "min"] | None = None
colorbar: bool | str | None = "auto"
colorbar_params: dict[str, object] | None = None
@dataclass
class ImageRenderParams:
"""Image render parameters.."""
cmap_params: list[CmapParams] | CmapParams
element: str
channel: list[str] | list[int] | int | str | None = None
palette: ListedColormap | list[str] | None = None
alpha: float = 1.0
scale: str | None = None
zorder: int = 0
colorbar: bool | str | None = "auto"
colorbar_params: dict[str, object] | None = None
transfunc: Callable[[np.ndarray], np.ndarray] | list[Callable[[np.ndarray], np.ndarray]] | None = None
grayscale: bool = False
@dataclass
class LabelsRenderParams:
"""Labels render parameters.."""
cmap_params: CmapParams
element: str
color: Color | None = None
col_for_color: str | None = None
groups: str | list[str] | None = None
contour_px: int | None = None
outline: bool = False
palette: ListedColormap | list[str] | None = None
outline_alpha: float = 1.0
outline_color: Color | None = None
fill_alpha: float = 0.4
transfunc: Callable[[float], float] | None = None
scale: str | None = None
table_name: str | None = None
table_layer: str | None = None
zorder: int = 0
colorbar: bool | str | None = "auto"
colorbar_params: dict[str, object] | None = None