scales-python 1.4.0.9000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scales/__init__.py +295 -0
- scales/_colors.py +272 -0
- scales/_palettes_data.py +595 -0
- scales/_utils.py +579 -0
- scales/bounds.py +512 -0
- scales/breaks.py +627 -0
- scales/breaks_log.py +268 -0
- scales/colour_manip.py +681 -0
- scales/colour_mapping.py +593 -0
- scales/colour_ramp.py +126 -0
- scales/labels.py +2144 -0
- scales/minor_breaks.py +197 -0
- scales/palettes.py +1328 -0
- scales/py.typed +0 -0
- scales/range.py +223 -0
- scales/scale_continuous.py +146 -0
- scales/scale_discrete.py +196 -0
- scales/transforms.py +1338 -0
- scales_python-1.4.0.9000.dist-info/METADATA +73 -0
- scales_python-1.4.0.9000.dist-info/RECORD +22 -0
- scales_python-1.4.0.9000.dist-info/WHEEL +4 -0
- scales_python-1.4.0.9000.dist-info/licenses/LICENSE +3 -0
scales/palettes.py
ADDED
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Palette functions for the scales package.
|
|
3
|
+
|
|
4
|
+
Python port of the R scales palette system, covering:
|
|
5
|
+
- R/pal-.R (core palette classes)
|
|
6
|
+
- R/pal-brewer.R (ColorBrewer palettes)
|
|
7
|
+
- R/pal-hue.R (HCL hue palettes)
|
|
8
|
+
- R/pal-viridis.R (viridis family)
|
|
9
|
+
- R/pal-gradient.R (gradient palettes)
|
|
10
|
+
- R/pal-grey.R (grey palettes)
|
|
11
|
+
- R/pal-area.R (area scaling)
|
|
12
|
+
- R/pal-shape.r (shape palettes)
|
|
13
|
+
- R/pal-linetype.R (linetype palettes)
|
|
14
|
+
- R/pal-identity.R (identity palette)
|
|
15
|
+
- R/pal-manual.R (manual palettes)
|
|
16
|
+
- R/pal-rescale.R (rescale palettes)
|
|
17
|
+
- R/pal-dichromat.R (colorblind-safe palettes)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import warnings
|
|
23
|
+
from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.typing import ArrayLike
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Core classes
|
|
30
|
+
"ContinuousPalette",
|
|
31
|
+
"DiscretePalette",
|
|
32
|
+
# Constructors
|
|
33
|
+
"new_continuous_palette",
|
|
34
|
+
"new_discrete_palette",
|
|
35
|
+
# Testing / getters
|
|
36
|
+
"is_pal",
|
|
37
|
+
"is_continuous_pal",
|
|
38
|
+
"is_discrete_pal",
|
|
39
|
+
"is_colour_pal",
|
|
40
|
+
"is_numeric_pal",
|
|
41
|
+
"palette_nlevels",
|
|
42
|
+
"palette_na_safe",
|
|
43
|
+
"palette_type",
|
|
44
|
+
# Coercion
|
|
45
|
+
"as_discrete_pal",
|
|
46
|
+
"as_continuous_pal",
|
|
47
|
+
# Registry (port of palette-registry.R)
|
|
48
|
+
"register_palette",
|
|
49
|
+
"get_palette",
|
|
50
|
+
"palette_names",
|
|
51
|
+
"reset_palettes",
|
|
52
|
+
# Discrete palette factories
|
|
53
|
+
"pal_brewer",
|
|
54
|
+
"pal_hue",
|
|
55
|
+
"pal_viridis",
|
|
56
|
+
"pal_grey",
|
|
57
|
+
"pal_shape",
|
|
58
|
+
"pal_linetype",
|
|
59
|
+
"pal_identity",
|
|
60
|
+
"pal_manual",
|
|
61
|
+
"pal_dichromat",
|
|
62
|
+
# Continuous palette factories
|
|
63
|
+
"pal_gradient_n",
|
|
64
|
+
"pal_div_gradient",
|
|
65
|
+
"pal_seq_gradient",
|
|
66
|
+
"pal_area",
|
|
67
|
+
"pal_rescale",
|
|
68
|
+
"abs_area",
|
|
69
|
+
# Legacy aliases
|
|
70
|
+
"brewer_pal",
|
|
71
|
+
"hue_pal",
|
|
72
|
+
"viridis_pal",
|
|
73
|
+
"grey_pal",
|
|
74
|
+
"shape_pal",
|
|
75
|
+
"linetype_pal",
|
|
76
|
+
"identity_pal",
|
|
77
|
+
"manual_pal",
|
|
78
|
+
"dichromat_pal",
|
|
79
|
+
"gradient_n_pal",
|
|
80
|
+
"div_gradient_pal",
|
|
81
|
+
"seq_gradient_pal",
|
|
82
|
+
"area_pal",
|
|
83
|
+
"rescale_pal",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# HCL -> RGB helper
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def _hcl_to_hex(h: float, c: float, l: float) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Convert a single HCL (CIE-LCh(uv) / polarLUV) colour to a hex string.
|
|
94
|
+
|
|
95
|
+
Mirrors R ``grDevices::hcl()`` which uses ``polarLUV()`` — i.e.
|
|
96
|
+
the **CIE L\\*C\\*h(uv)** cylindrical representation of **CIE L\\*u\\*v\\***,
|
|
97
|
+
not L\\*C\\*h(ab) / L\\*a\\*b\\*. The two spaces differ: using Lab for R's
|
|
98
|
+
hue_pal() produces over-saturated colours (e.g. ``#FF0076`` instead
|
|
99
|
+
of R's ``#F8766D``).
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
h : float
|
|
104
|
+
Hue angle in degrees (0--360).
|
|
105
|
+
c : float
|
|
106
|
+
Chroma (0--100+).
|
|
107
|
+
l : float
|
|
108
|
+
Luminance (0--100).
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
str
|
|
113
|
+
Hex colour string, e.g. ``"#F8766D"``.
|
|
114
|
+
"""
|
|
115
|
+
# --- polarLUV -> LUV --------------------------------------------------
|
|
116
|
+
h_rad = np.radians(h % 360)
|
|
117
|
+
u = c * np.cos(h_rad)
|
|
118
|
+
v = c * np.sin(h_rad)
|
|
119
|
+
|
|
120
|
+
# --- LUV -> XYZ (D65 white point) ------------------------------------
|
|
121
|
+
# Reference white (D65): X_n=0.95047, Y_n=1.00000, Z_n=1.08883
|
|
122
|
+
Xn, Yn, Zn = 0.95047, 1.00000, 1.08883
|
|
123
|
+
denom_n = Xn + 15.0 * Yn + 3.0 * Zn
|
|
124
|
+
un_prime = 4.0 * Xn / denom_n # ≈ 0.19783
|
|
125
|
+
vn_prime = 9.0 * Yn / denom_n # ≈ 0.46830
|
|
126
|
+
|
|
127
|
+
# L* -> Y: piecewise (CIE L* definition)
|
|
128
|
+
eps = 216.0 / 24389.0 # 0.008856
|
|
129
|
+
kappa = 24389.0 / 27.0 # 903.2962962...
|
|
130
|
+
if l > kappa * eps: # l > 8
|
|
131
|
+
y_rel = ((l + 16.0) / 116.0) ** 3
|
|
132
|
+
else:
|
|
133
|
+
y_rel = l / kappa
|
|
134
|
+
y = y_rel * Yn
|
|
135
|
+
|
|
136
|
+
# u, v -> u', v' (only valid when L* > 0)
|
|
137
|
+
if l <= 0.0:
|
|
138
|
+
x = y_val = z = 0.0
|
|
139
|
+
else:
|
|
140
|
+
u_prime = u / (13.0 * l) + un_prime
|
|
141
|
+
v_prime = v / (13.0 * l) + vn_prime
|
|
142
|
+
if v_prime == 0.0:
|
|
143
|
+
x = y_val = z = 0.0
|
|
144
|
+
else:
|
|
145
|
+
x = y * (9.0 * u_prime) / (4.0 * v_prime)
|
|
146
|
+
z = y * (12.0 - 3.0 * u_prime - 20.0 * v_prime) / (4.0 * v_prime)
|
|
147
|
+
y_val = y
|
|
148
|
+
|
|
149
|
+
# --- XYZ -> linear sRGB ----------------------------------------------
|
|
150
|
+
rl = 3.2404542 * x - 1.5371385 * y_val - 0.4985314 * z
|
|
151
|
+
gl = -0.9692660 * x + 1.8760108 * y_val + 0.0415560 * z
|
|
152
|
+
bl = 0.0556434 * x - 0.2040259 * y_val + 1.0572252 * z
|
|
153
|
+
|
|
154
|
+
# Gamma companding (sRGB)
|
|
155
|
+
def _gamma(val: float) -> float:
|
|
156
|
+
if val <= 0.0031308:
|
|
157
|
+
return 12.92 * val
|
|
158
|
+
return 1.055 * (val ** (1.0 / 2.4)) - 0.055
|
|
159
|
+
|
|
160
|
+
r = np.clip(_gamma(rl), 0.0, 1.0)
|
|
161
|
+
g = np.clip(_gamma(gl), 0.0, 1.0)
|
|
162
|
+
b_val = np.clip(_gamma(bl), 0.0, 1.0)
|
|
163
|
+
|
|
164
|
+
return "#{:02X}{:02X}{:02X}".format(
|
|
165
|
+
int(round(r * 255)),
|
|
166
|
+
int(round(g * 255)),
|
|
167
|
+
int(round(b_val * 255)),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Embedded dichromat colour schemes
|
|
173
|
+
#
|
|
174
|
+
# Exact port of R's ``dichromat::colorschemes`` (all 17 palettes) — see
|
|
175
|
+
# the canonical source:
|
|
176
|
+
# https://github.com/cran/dichromat/blob/master/R/colorschemes.R
|
|
177
|
+
# Reference: Light A, Bartlein PJ (2004) "The End of the Rainbow?",
|
|
178
|
+
# EOS Trans. AGU 85(40):385; data via Scott Waichler / Univ. of Oregon
|
|
179
|
+
# (https://geography.uoregon.edu/datagraphics/color_scales.htm).
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
_DICHROMAT_SCHEMES: dict[str, list[str]] = {
|
|
183
|
+
"BrowntoBlue.10": [
|
|
184
|
+
"#663000", "#996136", "#CC9B7A", "#D9AF98", "#F2DACE",
|
|
185
|
+
"#CCFDFF", "#99F8FF", "#66F0FF", "#33E4FF", "#00AACC",
|
|
186
|
+
],
|
|
187
|
+
"BrowntoBlue.12": [
|
|
188
|
+
"#331A00", "#663000", "#996136", "#CC9B7A", "#D9AF98",
|
|
189
|
+
"#F2DACE", "#CCFDFF", "#99F8FF", "#66F0FF", "#33E4FF",
|
|
190
|
+
"#00AACC", "#007A99",
|
|
191
|
+
],
|
|
192
|
+
"BluetoDarkOrange.12": [
|
|
193
|
+
"#1F8F99", "#52C4CC", "#99FAFF", "#B2FCFF", "#CCFEFF",
|
|
194
|
+
"#E6FFFF", "#FFE6CC", "#FFCA99", "#FFAD66", "#FF8F33",
|
|
195
|
+
"#CC5800", "#994000",
|
|
196
|
+
],
|
|
197
|
+
"BluetoDarkOrange.18": [
|
|
198
|
+
"#006666", "#009999", "#00CCCC", "#00FFFF", "#33FFFF",
|
|
199
|
+
"#66FFFF", "#99FFFF", "#B2FFFF", "#CCFFFF", "#E6FFFF",
|
|
200
|
+
"#FFE6CC", "#FFCA99", "#FFAD66", "#FF8F33", "#FF6E00",
|
|
201
|
+
"#CC5500", "#993D00", "#662700",
|
|
202
|
+
],
|
|
203
|
+
"DarkRedtoBlue.12": [
|
|
204
|
+
"#2A0BD9", "#264EFF", "#40A1FF", "#73DAFF", "#ABF8FF",
|
|
205
|
+
"#E0FFFF", "#FFFFBF", "#FFE099", "#FFAD73", "#F76E5E",
|
|
206
|
+
"#D92632", "#A60021",
|
|
207
|
+
],
|
|
208
|
+
"DarkRedtoBlue.18": [
|
|
209
|
+
"#2400D9", "#191DF7", "#2957FF", "#3D87FF", "#57B0FF",
|
|
210
|
+
"#75D3FF", "#99EBFF", "#BDF9FF", "#EBFFFF", "#FFFFEB",
|
|
211
|
+
"#FFF2BD", "#FFD699", "#FFAC75", "#FF7857", "#FF3D3D",
|
|
212
|
+
"#F72836", "#D91630", "#A60021",
|
|
213
|
+
],
|
|
214
|
+
"BluetoGreen.14": [
|
|
215
|
+
"#0000FF", "#3333FF", "#6666FF", "#9999FF", "#B2B2FF",
|
|
216
|
+
"#CCCCFF", "#E6E6FF", "#E6FFE6", "#CCFFCC", "#B2FFB2",
|
|
217
|
+
"#99FF99", "#66FF66", "#33FF33", "#00FF00",
|
|
218
|
+
],
|
|
219
|
+
"BluetoGray.8": [
|
|
220
|
+
"#0099CC", "#66E6FF", "#99FFFF", "#CCFFFF", "#E6E6E6",
|
|
221
|
+
"#999999", "#666666", "#333333",
|
|
222
|
+
],
|
|
223
|
+
"BluetoOrangeRed.14": [
|
|
224
|
+
"#085AFF", "#3377FF", "#5991FF", "#8CB2FF", "#BFD4FF",
|
|
225
|
+
"#E6EEFF", "#F7FAFF", "#FFFFCC", "#FFFF99", "#FFFF00",
|
|
226
|
+
"#FFCC00", "#FF9900", "#FF6600", "#FF0000",
|
|
227
|
+
],
|
|
228
|
+
"BluetoOrange.10": [
|
|
229
|
+
"#0055FF", "#3399FF", "#66CCFF", "#99EEFF", "#CCFFFF",
|
|
230
|
+
"#FFFFCC", "#FFEE99", "#FFCC66", "#FF9933", "#FF5500",
|
|
231
|
+
],
|
|
232
|
+
"BluetoOrange.12": [
|
|
233
|
+
"#002BFF", "#1A66FF", "#3399FF", "#66CCFF", "#99EEFF",
|
|
234
|
+
"#CCFFFF", "#FFFFCC", "#FFEE99", "#FFCC66", "#FF9933",
|
|
235
|
+
"#FF661A", "#FF2B00",
|
|
236
|
+
],
|
|
237
|
+
"BluetoOrange.8": [
|
|
238
|
+
"#0080FF", "#4CC4FF", "#99EEFF", "#CCFFFF", "#FFFFCC",
|
|
239
|
+
"#FFEE99", "#FFC44C", "#FF8000",
|
|
240
|
+
],
|
|
241
|
+
"LightBluetoDarkBlue.10": [
|
|
242
|
+
"#E6FFFF", "#CCFBFF", "#B2F2FF", "#99E6FF", "#80D4FF",
|
|
243
|
+
"#66BFFF", "#4CA6FF", "#3388FF", "#1A66FF", "#0040FF",
|
|
244
|
+
],
|
|
245
|
+
"LightBluetoDarkBlue.7": [
|
|
246
|
+
"#FFFFFF", "#CCFDFF", "#99F8FF", "#66F0FF", "#33E4FF",
|
|
247
|
+
"#00AACC", "#007A99",
|
|
248
|
+
],
|
|
249
|
+
"Categorical.12": [
|
|
250
|
+
"#FFBF80", "#FF8000", "#FFFF99", "#FFFF33", "#B2FF8C",
|
|
251
|
+
"#33FF00", "#A6EDFF", "#1AB2FF", "#CCBFFF", "#664CFF",
|
|
252
|
+
"#FF99BF", "#E61A33",
|
|
253
|
+
],
|
|
254
|
+
"GreentoMagenta.16": [
|
|
255
|
+
"#005100", "#008600", "#00BC00", "#00F100", "#51FF51",
|
|
256
|
+
"#86FF86", "#BCFFBC", "#FFFFFF", "#FFF1FF", "#FFBCFF",
|
|
257
|
+
"#FF86FF", "#FF51FF", "#F100F1", "#BC00BC", "#860086",
|
|
258
|
+
"#510051",
|
|
259
|
+
],
|
|
260
|
+
"SteppedSequential.5": [
|
|
261
|
+
"#990F0F", "#B22D2D", "#CC5252", "#E67E7E", "#FFB2B2",
|
|
262
|
+
"#99700F", "#B28B2D", "#CCA852", "#E6C77E", "#FFE8B2",
|
|
263
|
+
"#1F990F", "#3CB22D", "#60CC52", "#8AE67E", "#BCFFB2",
|
|
264
|
+
"#710F99", "#8B2DB2", "#A852CC", "#C77EE6", "#E9B2FF",
|
|
265
|
+
"#990F20", "#B22D3C", "#CC5260", "#E67E8A", "#FFB2BC",
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Linetype values (matching R linetype names)
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
_LINETYPES: list[str] = [
|
|
274
|
+
"solid",
|
|
275
|
+
"dashed",
|
|
276
|
+
"dotted",
|
|
277
|
+
"dotdash",
|
|
278
|
+
"longdash",
|
|
279
|
+
"twodash",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Viridis option -> matplotlib cmap name
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
_VIRIDIS_OPTIONS: dict[str, str] = {
|
|
287
|
+
"A": "magma",
|
|
288
|
+
"B": "inferno",
|
|
289
|
+
"C": "plasma",
|
|
290
|
+
"D": "viridis",
|
|
291
|
+
"E": "cividis",
|
|
292
|
+
"F": "rocket",
|
|
293
|
+
"G": "mako",
|
|
294
|
+
"H": "turbo",
|
|
295
|
+
"magma": "magma",
|
|
296
|
+
"inferno": "inferno",
|
|
297
|
+
"plasma": "plasma",
|
|
298
|
+
"viridis": "viridis",
|
|
299
|
+
"cividis": "cividis",
|
|
300
|
+
"rocket": "rocket",
|
|
301
|
+
"mako": "mako",
|
|
302
|
+
"turbo": "turbo",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ===================================================================
|
|
307
|
+
# Core palette classes
|
|
308
|
+
# ===================================================================
|
|
309
|
+
|
|
310
|
+
class ContinuousPalette:
|
|
311
|
+
"""
|
|
312
|
+
A palette backed by a function mapping ``[0, 1]`` to colours/values.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
fun : callable
|
|
317
|
+
A function that accepts an array of values in ``[0, 1]`` and returns
|
|
318
|
+
a corresponding array of colours or numeric values.
|
|
319
|
+
type : str
|
|
320
|
+
Either ``"colour"`` or ``"numeric"``.
|
|
321
|
+
na_safe : bool or None, optional
|
|
322
|
+
Whether the palette function handles ``NaN`` values gracefully.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
fun: Callable[..., Any],
|
|
328
|
+
type: str,
|
|
329
|
+
na_safe: Optional[bool] = None,
|
|
330
|
+
) -> None:
|
|
331
|
+
self._fun = fun
|
|
332
|
+
self.type = type
|
|
333
|
+
self.na_safe = na_safe
|
|
334
|
+
|
|
335
|
+
def __call__(self, x: Any) -> Any:
|
|
336
|
+
"""Evaluate the palette at *x* (values in ``[0, 1]``)."""
|
|
337
|
+
return self._fun(x)
|
|
338
|
+
|
|
339
|
+
def __repr__(self) -> str:
|
|
340
|
+
return (
|
|
341
|
+
f"ContinuousPalette(type={self.type!r}, na_safe={self.na_safe!r})"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class DiscretePalette:
|
|
346
|
+
"""
|
|
347
|
+
A palette backed by a function mapping an integer *n* to *n* values.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
fun : callable
|
|
352
|
+
A function that accepts a positive integer *n* and returns a list or
|
|
353
|
+
array of *n* colours or values.
|
|
354
|
+
type : str
|
|
355
|
+
Either ``"colour"`` or ``"numeric"``.
|
|
356
|
+
nlevels : int or None, optional
|
|
357
|
+
Maximum number of levels the palette supports.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
def __init__(
|
|
361
|
+
self,
|
|
362
|
+
fun: Callable[..., Any],
|
|
363
|
+
type: str,
|
|
364
|
+
nlevels: Optional[int] = None,
|
|
365
|
+
) -> None:
|
|
366
|
+
self._fun = fun
|
|
367
|
+
self.type = type
|
|
368
|
+
self.nlevels = nlevels
|
|
369
|
+
|
|
370
|
+
def __call__(self, n: int) -> Any:
|
|
371
|
+
"""Return *n* palette values."""
|
|
372
|
+
return self._fun(n)
|
|
373
|
+
|
|
374
|
+
def __repr__(self) -> str:
|
|
375
|
+
return (
|
|
376
|
+
f"DiscretePalette(type={self.type!r}, nlevels={self.nlevels!r})"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ===================================================================
|
|
381
|
+
# Constructors
|
|
382
|
+
# ===================================================================
|
|
383
|
+
|
|
384
|
+
def new_continuous_palette(
|
|
385
|
+
fun: Callable[..., Any],
|
|
386
|
+
type: str,
|
|
387
|
+
na_safe: Optional[bool] = None,
|
|
388
|
+
) -> ContinuousPalette:
|
|
389
|
+
"""
|
|
390
|
+
Create a new continuous palette.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
fun : callable
|
|
395
|
+
Function mapping values in ``[0, 1]`` to colours or numbers.
|
|
396
|
+
type : str
|
|
397
|
+
``"colour"`` or ``"numeric"``.
|
|
398
|
+
na_safe : bool or None, optional
|
|
399
|
+
Whether *fun* handles ``NaN`` gracefully.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
ContinuousPalette
|
|
404
|
+
"""
|
|
405
|
+
return ContinuousPalette(fun, type, na_safe=na_safe)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def new_discrete_palette(
|
|
409
|
+
fun: Callable[..., Any],
|
|
410
|
+
type: str,
|
|
411
|
+
nlevels: Optional[int] = None,
|
|
412
|
+
) -> DiscretePalette:
|
|
413
|
+
"""
|
|
414
|
+
Create a new discrete palette.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
fun : callable
|
|
419
|
+
Function mapping an integer *n* to a sequence of *n* values.
|
|
420
|
+
type : str
|
|
421
|
+
``"colour"`` or ``"numeric"``.
|
|
422
|
+
nlevels : int or None, optional
|
|
423
|
+
Maximum number of levels the palette supports.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
DiscretePalette
|
|
428
|
+
"""
|
|
429
|
+
return DiscretePalette(fun, type, nlevels=nlevels)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ===================================================================
|
|
433
|
+
# Testing / getters
|
|
434
|
+
# ===================================================================
|
|
435
|
+
|
|
436
|
+
def is_pal(x: Any) -> bool:
|
|
437
|
+
"""Return ``True`` if *x* is a palette object."""
|
|
438
|
+
return isinstance(x, (ContinuousPalette, DiscretePalette))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def is_continuous_pal(x: Any) -> bool:
|
|
442
|
+
"""Return ``True`` if *x* is a continuous palette."""
|
|
443
|
+
return isinstance(x, ContinuousPalette)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def is_discrete_pal(x: Any) -> bool:
|
|
447
|
+
"""Return ``True`` if *x* is a discrete palette."""
|
|
448
|
+
return isinstance(x, DiscretePalette)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def is_colour_pal(x: Any) -> bool:
|
|
452
|
+
"""Return ``True`` if *x* is a colour palette."""
|
|
453
|
+
return is_pal(x) and getattr(x, "type", None) == "colour"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def is_numeric_pal(x: Any) -> bool:
|
|
457
|
+
"""Return ``True`` if *x* is a numeric palette."""
|
|
458
|
+
return is_pal(x) and getattr(x, "type", None) == "numeric"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def palette_nlevels(pal: DiscretePalette) -> Optional[int]:
|
|
462
|
+
"""
|
|
463
|
+
Return the maximum number of levels for a discrete palette.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
pal : DiscretePalette
|
|
468
|
+
A discrete palette.
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
int or None
|
|
473
|
+
"""
|
|
474
|
+
return getattr(pal, "nlevels", None)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def palette_na_safe(pal: ContinuousPalette) -> Optional[bool]:
|
|
478
|
+
"""
|
|
479
|
+
Return whether a continuous palette handles ``NaN`` safely.
|
|
480
|
+
|
|
481
|
+
Parameters
|
|
482
|
+
----------
|
|
483
|
+
pal : ContinuousPalette
|
|
484
|
+
A continuous palette.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
bool or None
|
|
489
|
+
"""
|
|
490
|
+
return getattr(pal, "na_safe", None)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def palette_type(pal: Union[ContinuousPalette, DiscretePalette]) -> str:
|
|
494
|
+
"""
|
|
495
|
+
Return the type of a palette (``"colour"`` or ``"numeric"``).
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
pal : ContinuousPalette or DiscretePalette
|
|
500
|
+
A palette object.
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
str
|
|
505
|
+
"""
|
|
506
|
+
return getattr(pal, "type", "unknown")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ===================================================================
|
|
510
|
+
# Coercion
|
|
511
|
+
# ===================================================================
|
|
512
|
+
|
|
513
|
+
def as_discrete_pal(x: Any) -> DiscretePalette:
|
|
514
|
+
"""
|
|
515
|
+
Coerce *x* to a discrete palette.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
x : DiscretePalette, ContinuousPalette, or str
|
|
520
|
+
If already a :class:`DiscretePalette`, returned as-is.
|
|
521
|
+
If a :class:`ContinuousPalette`, samples *n* evenly-spaced values.
|
|
522
|
+
If a ``str``, looks up by name (currently supports ``"viridis"``,
|
|
523
|
+
``"brewer"`` family, and dichromat scheme names).
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
DiscretePalette
|
|
528
|
+
"""
|
|
529
|
+
if isinstance(x, DiscretePalette):
|
|
530
|
+
return x
|
|
531
|
+
|
|
532
|
+
if isinstance(x, ContinuousPalette):
|
|
533
|
+
pal_type = x.type
|
|
534
|
+
|
|
535
|
+
def _sampler(n: int, _cont=x) -> Any:
|
|
536
|
+
if n == 1:
|
|
537
|
+
points = np.array([0.5])
|
|
538
|
+
else:
|
|
539
|
+
points = np.linspace(0.0, 1.0, n)
|
|
540
|
+
return _cont(points)
|
|
541
|
+
|
|
542
|
+
return DiscretePalette(_sampler, type=pal_type)
|
|
543
|
+
|
|
544
|
+
if isinstance(x, str):
|
|
545
|
+
# Consult the global registry (mirrors R's get_palette path).
|
|
546
|
+
try:
|
|
547
|
+
pal = get_palette(x)
|
|
548
|
+
except KeyError as err:
|
|
549
|
+
raise ValueError(f"Unknown palette name: {x!r}") from err
|
|
550
|
+
if isinstance(pal, DiscretePalette):
|
|
551
|
+
return pal
|
|
552
|
+
if isinstance(pal, ContinuousPalette):
|
|
553
|
+
# Recurse through the ContinuousPalette branch to derive a
|
|
554
|
+
# discrete sampler.
|
|
555
|
+
return as_discrete_pal(pal)
|
|
556
|
+
raise ValueError(f"Unknown palette name: {x!r}")
|
|
557
|
+
|
|
558
|
+
raise TypeError(f"Cannot coerce {type(x).__name__} to DiscretePalette")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def as_continuous_pal(x: Any) -> ContinuousPalette:
|
|
562
|
+
"""
|
|
563
|
+
Coerce *x* to a continuous palette.
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
x : ContinuousPalette, DiscretePalette, or str
|
|
568
|
+
If already a :class:`ContinuousPalette`, returned as-is.
|
|
569
|
+
If a :class:`DiscretePalette`, samples a set of colours and
|
|
570
|
+
interpolates between them.
|
|
571
|
+
If a ``str``, looks up by name.
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
ContinuousPalette
|
|
576
|
+
"""
|
|
577
|
+
if isinstance(x, ContinuousPalette):
|
|
578
|
+
return x
|
|
579
|
+
|
|
580
|
+
if isinstance(x, DiscretePalette):
|
|
581
|
+
# Sample enough colours and build a gradient through them
|
|
582
|
+
n_sample = max(palette_nlevels(x) or 7, 7)
|
|
583
|
+
colours = x(n_sample)
|
|
584
|
+
return pal_gradient_n(colours)
|
|
585
|
+
|
|
586
|
+
if isinstance(x, str):
|
|
587
|
+
# Consult the registry first — it may hold a continuous palette
|
|
588
|
+
# (viridis variants) or a discrete one (Brewer).
|
|
589
|
+
try:
|
|
590
|
+
pal = get_palette(x)
|
|
591
|
+
except KeyError as err:
|
|
592
|
+
raise ValueError(f"Unknown palette name: {x!r}") from err
|
|
593
|
+
if isinstance(pal, ContinuousPalette):
|
|
594
|
+
return pal
|
|
595
|
+
if isinstance(pal, DiscretePalette):
|
|
596
|
+
return as_continuous_pal(pal)
|
|
597
|
+
raise ValueError(f"Unknown palette name: {x!r}")
|
|
598
|
+
|
|
599
|
+
raise TypeError(f"Cannot coerce {type(x).__name__} to ContinuousPalette")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# ===================================================================
|
|
603
|
+
# Discrete palette factories
|
|
604
|
+
# ===================================================================
|
|
605
|
+
|
|
606
|
+
def pal_brewer(
|
|
607
|
+
type: str = "seq",
|
|
608
|
+
palette: Union[int, str] = 1,
|
|
609
|
+
direction: int = 1,
|
|
610
|
+
) -> DiscretePalette:
|
|
611
|
+
"""
|
|
612
|
+
ColorBrewer palettes.
|
|
613
|
+
|
|
614
|
+
Parameters
|
|
615
|
+
----------
|
|
616
|
+
type : str, optional
|
|
617
|
+
One of ``"seq"`` (sequential), ``"div"`` (diverging), or ``"qual"``
|
|
618
|
+
(qualitative). Default ``"seq"``.
|
|
619
|
+
palette : int or str, optional
|
|
620
|
+
Palette index (1-based) or name (e.g. ``"Blues"``, ``"Set1"``).
|
|
621
|
+
Default ``1``.
|
|
622
|
+
direction : int, optional
|
|
623
|
+
``1`` for normal order, ``-1`` to reverse. Default ``1``.
|
|
624
|
+
|
|
625
|
+
Returns
|
|
626
|
+
-------
|
|
627
|
+
DiscretePalette
|
|
628
|
+
"""
|
|
629
|
+
from ._palettes_data import BREWER, BREWER_MAXCOLORS, BREWER_TYPES
|
|
630
|
+
|
|
631
|
+
_TYPE_PALETTES: dict[str, list[str]] = {
|
|
632
|
+
t: [k for k, v in sorted(BREWER_TYPES.items()) if v == t]
|
|
633
|
+
for t in ("seq", "div", "qual")
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if isinstance(palette, int):
|
|
637
|
+
names = _TYPE_PALETTES.get(type, _TYPE_PALETTES["seq"])
|
|
638
|
+
idx = max(0, min(palette - 1, len(names) - 1))
|
|
639
|
+
cmap_name = names[idx]
|
|
640
|
+
else:
|
|
641
|
+
cmap_name = palette
|
|
642
|
+
|
|
643
|
+
max_n = BREWER_MAXCOLORS.get(cmap_name, 9)
|
|
644
|
+
pal_data = BREWER.get(cmap_name, BREWER["Greens"])
|
|
645
|
+
|
|
646
|
+
def _brewer_fun(n: int) -> list[str]:
|
|
647
|
+
# R: brewer.pal(n, pal) returns the pre-designed n-colour subset.
|
|
648
|
+
# If n < 3, R returns the 3-colour subset (suppresses warning).
|
|
649
|
+
# If n > maxcolors, R returns maxcolors colours.
|
|
650
|
+
n_lookup = max(3, min(n, max_n))
|
|
651
|
+
|
|
652
|
+
# pal_data is {n: [colours]} — exact R brewer.pal(n, name) output
|
|
653
|
+
colours = list(pal_data.get(n_lookup, pal_data[max_n]))
|
|
654
|
+
|
|
655
|
+
# Take first n colours (for n < 3 case)
|
|
656
|
+
colours = colours[:min(n, max_n)]
|
|
657
|
+
|
|
658
|
+
# Pad with None (NA) if n > maxcolors
|
|
659
|
+
while len(colours) < n:
|
|
660
|
+
colours.append(None)
|
|
661
|
+
|
|
662
|
+
if direction == -1:
|
|
663
|
+
colours = colours[::-1]
|
|
664
|
+
return colours
|
|
665
|
+
|
|
666
|
+
return DiscretePalette(_brewer_fun, type="colour", nlevels=max_n)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def pal_hue(
|
|
670
|
+
h: Tuple[float, float] = (15, 375),
|
|
671
|
+
c: float = 100,
|
|
672
|
+
l: float = 65,
|
|
673
|
+
h_start: float = 0,
|
|
674
|
+
direction: int = 1,
|
|
675
|
+
) -> DiscretePalette:
|
|
676
|
+
"""
|
|
677
|
+
Evenly-spaced HCL hue palette.
|
|
678
|
+
|
|
679
|
+
Divides the hue range into *n* equally-spaced segments and converts each
|
|
680
|
+
HCL triplet to an sRGB hex colour.
|
|
681
|
+
|
|
682
|
+
Parameters
|
|
683
|
+
----------
|
|
684
|
+
h : tuple of float, optional
|
|
685
|
+
Hue range in degrees. Default ``(15, 375)``.
|
|
686
|
+
c : float, optional
|
|
687
|
+
Chroma. Default ``100``.
|
|
688
|
+
l : float, optional
|
|
689
|
+
Luminance. Default ``65``.
|
|
690
|
+
h_start : float, optional
|
|
691
|
+
Starting hue offset in degrees. Default ``0``.
|
|
692
|
+
direction : int, optional
|
|
693
|
+
``1`` for increasing hue, ``-1`` for decreasing. Default ``1``.
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
DiscretePalette
|
|
698
|
+
"""
|
|
699
|
+
|
|
700
|
+
def _hue_fun(n: int) -> list[str]:
|
|
701
|
+
if n == 0:
|
|
702
|
+
raise ValueError(
|
|
703
|
+
"Must request at least one colour from a hue palette."
|
|
704
|
+
)
|
|
705
|
+
# Mirror R: only collapse the endpoint when the hue range is a
|
|
706
|
+
# full 360° circle (i.e. `diff(h) %% 360 < 1`) — otherwise the
|
|
707
|
+
# endpoint is a genuine boundary and must be included.
|
|
708
|
+
h0, h1 = float(h[0]), float(h[1])
|
|
709
|
+
if ((h1 - h0) % 360) < 1:
|
|
710
|
+
h1_used = h1 - 360.0 / n
|
|
711
|
+
hues = np.linspace(h0, h1_used, n)
|
|
712
|
+
else:
|
|
713
|
+
hues = np.linspace(h0, h1, n)
|
|
714
|
+
hues = (hues + h_start) % 360
|
|
715
|
+
result = [_hcl_to_hex(hue, c, l) for hue in hues]
|
|
716
|
+
if direction == -1:
|
|
717
|
+
result = result[::-1]
|
|
718
|
+
return result
|
|
719
|
+
|
|
720
|
+
return DiscretePalette(_hue_fun, type="colour", nlevels=255)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def pal_viridis(
|
|
724
|
+
alpha: float = 1,
|
|
725
|
+
begin: float = 0,
|
|
726
|
+
end: float = 1,
|
|
727
|
+
direction: int = 1,
|
|
728
|
+
option: str = "D",
|
|
729
|
+
) -> DiscretePalette:
|
|
730
|
+
"""
|
|
731
|
+
Viridis family colour palettes.
|
|
732
|
+
|
|
733
|
+
Parameters
|
|
734
|
+
----------
|
|
735
|
+
alpha : float, optional
|
|
736
|
+
Opacity (0--1). Default ``1``.
|
|
737
|
+
begin : float, optional
|
|
738
|
+
Start of colour map range (0--1). Default ``0``.
|
|
739
|
+
end : float, optional
|
|
740
|
+
End of colour map range (0--1). Default ``1``.
|
|
741
|
+
direction : int, optional
|
|
742
|
+
``1`` for normal, ``-1`` for reversed. Default ``1``.
|
|
743
|
+
option : str, optional
|
|
744
|
+
Colour map variant. One of ``"A"``/``"magma"``, ``"B"``/``"inferno"``,
|
|
745
|
+
``"C"``/``"plasma"``, ``"D"``/``"viridis"`` (default),
|
|
746
|
+
``"E"``/``"cividis"``, ``"H"``/``"turbo"``.
|
|
747
|
+
|
|
748
|
+
Returns
|
|
749
|
+
-------
|
|
750
|
+
DiscretePalette
|
|
751
|
+
"""
|
|
752
|
+
from ._palettes_data import VIRIDIS
|
|
753
|
+
from ._colors import to_hex as _to_hex
|
|
754
|
+
|
|
755
|
+
cmap_name = _VIRIDIS_OPTIONS.get(option, "viridis")
|
|
756
|
+
cmap_data = VIRIDIS.get(cmap_name, VIRIDIS["viridis"]) # 256 hex colors
|
|
757
|
+
n_cmap = len(cmap_data)
|
|
758
|
+
|
|
759
|
+
def _viridis_fun(n: int) -> list[str]:
|
|
760
|
+
if n == 0:
|
|
761
|
+
return []
|
|
762
|
+
|
|
763
|
+
if direction == -1:
|
|
764
|
+
positions = np.linspace(end, begin, n)
|
|
765
|
+
else:
|
|
766
|
+
positions = np.linspace(begin, end, n)
|
|
767
|
+
|
|
768
|
+
colours: list[str] = []
|
|
769
|
+
for pos in positions:
|
|
770
|
+
idx = min(int(round(pos * (n_cmap - 1))), n_cmap - 1)
|
|
771
|
+
idx = max(0, idx)
|
|
772
|
+
hex_col = cmap_data[idx]
|
|
773
|
+
if alpha < 1:
|
|
774
|
+
# Parse hex and add alpha
|
|
775
|
+
from ._colors import to_rgba as _to_rgba
|
|
776
|
+
r, g, b, _ = _to_rgba(hex_col)
|
|
777
|
+
colours.append(_to_hex((r, g, b, alpha), keep_alpha=True))
|
|
778
|
+
else:
|
|
779
|
+
colours.append(hex_col.lower())
|
|
780
|
+
return colours
|
|
781
|
+
|
|
782
|
+
return DiscretePalette(_viridis_fun, type="colour")
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def pal_grey(
|
|
786
|
+
start: float = 0.2,
|
|
787
|
+
end: float = 0.8,
|
|
788
|
+
) -> DiscretePalette:
|
|
789
|
+
"""
|
|
790
|
+
Grey palette.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
start : float, optional
|
|
795
|
+
Starting grey level (0 = black, 1 = white). Default ``0.2``.
|
|
796
|
+
end : float, optional
|
|
797
|
+
Ending grey level. Default ``0.8``.
|
|
798
|
+
|
|
799
|
+
Returns
|
|
800
|
+
-------
|
|
801
|
+
DiscretePalette
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
def _grey_fun(n: int) -> list[str]:
|
|
805
|
+
if n == 0:
|
|
806
|
+
return []
|
|
807
|
+
if n == 1:
|
|
808
|
+
levels = [(start + end) / 2.0]
|
|
809
|
+
else:
|
|
810
|
+
levels = np.linspace(start, end, n).tolist()
|
|
811
|
+
return [
|
|
812
|
+
"#{0:02X}{0:02X}{0:02X}".format(int(round(lv * 255)))
|
|
813
|
+
for lv in levels
|
|
814
|
+
]
|
|
815
|
+
|
|
816
|
+
return DiscretePalette(_grey_fun, type="colour")
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def pal_shape(solid: bool = True) -> DiscretePalette:
|
|
820
|
+
"""
|
|
821
|
+
Shape palette.
|
|
822
|
+
|
|
823
|
+
Returns R plotting-code integers (compatible with ggplot2's shape
|
|
824
|
+
scale). Codes mirror R's ``pal_shape``:
|
|
825
|
+
|
|
826
|
+
* ``solid=True`` → ``[16, 17, 15, 3, 7, 8]``
|
|
827
|
+
* ``solid=False`` → ``[1, 2, 0, 3, 7, 8]``
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
solid : bool, optional
|
|
832
|
+
Use solid (filled) shapes if ``True`` (default) else open shapes.
|
|
833
|
+
|
|
834
|
+
Returns
|
|
835
|
+
-------
|
|
836
|
+
DiscretePalette
|
|
837
|
+
"""
|
|
838
|
+
# Per R's pal-shape.r:
|
|
839
|
+
# solid=TRUE -> c(16, 17, 15, 3, 7, 8)
|
|
840
|
+
# solid=FALSE -> c( 1, 2, 0, 3, 7, 8)
|
|
841
|
+
shapes = [16, 17, 15, 3, 7, 8] if solid else [1, 2, 0, 3, 7, 8]
|
|
842
|
+
max_n = len(shapes)
|
|
843
|
+
|
|
844
|
+
def _shape_fun(n: int) -> list[Optional[int]]:
|
|
845
|
+
if n > max_n:
|
|
846
|
+
# R: cli::cli_warn(...) — warn but do not abort; positions
|
|
847
|
+
# beyond max_n come back as NA (None in Python).
|
|
848
|
+
warnings.warn(
|
|
849
|
+
"The shape palette can deal with a maximum of 6 discrete "
|
|
850
|
+
f"values because more than 6 becomes difficult to "
|
|
851
|
+
f"discriminate; you have requested {n} values. Consider "
|
|
852
|
+
"specifying shapes manually if you need that many.",
|
|
853
|
+
stacklevel=2,
|
|
854
|
+
)
|
|
855
|
+
return [*shapes, *([None] * (n - max_n))]
|
|
856
|
+
return shapes[:n]
|
|
857
|
+
|
|
858
|
+
return DiscretePalette(_shape_fun, type="shape", nlevels=max_n)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def pal_linetype() -> DiscretePalette:
|
|
862
|
+
"""
|
|
863
|
+
Linetype palette.
|
|
864
|
+
|
|
865
|
+
Returns linetype name strings compatible with matplotlib linestyle
|
|
866
|
+
specification.
|
|
867
|
+
|
|
868
|
+
Returns
|
|
869
|
+
-------
|
|
870
|
+
DiscretePalette
|
|
871
|
+
"""
|
|
872
|
+
max_n = len(_LINETYPES)
|
|
873
|
+
|
|
874
|
+
def _linetype_fun(n: int) -> list[Optional[str]]:
|
|
875
|
+
if n > max_n:
|
|
876
|
+
# Per R's pal_manual (which pal_linetype wraps): warn rather
|
|
877
|
+
# than abort; positions past max_n come back as NA / None.
|
|
878
|
+
warnings.warn(
|
|
879
|
+
f"This manual palette can handle a maximum of {max_n} "
|
|
880
|
+
f"values. You have supplied {n}.",
|
|
881
|
+
stacklevel=2,
|
|
882
|
+
)
|
|
883
|
+
return [*_LINETYPES, *([None] * (n - max_n))]
|
|
884
|
+
return _LINETYPES[:n]
|
|
885
|
+
|
|
886
|
+
return DiscretePalette(_linetype_fun, type="linetype", nlevels=max_n)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def pal_identity() -> DiscretePalette:
|
|
890
|
+
"""
|
|
891
|
+
Identity palette.
|
|
892
|
+
|
|
893
|
+
Returns the input values unchanged. Mirrors R's
|
|
894
|
+
``pal_identity <- function() function(x) x``: the returned palette
|
|
895
|
+
is a pass-through that echoes whatever is passed to it.
|
|
896
|
+
|
|
897
|
+
Returns
|
|
898
|
+
-------
|
|
899
|
+
DiscretePalette
|
|
900
|
+
"""
|
|
901
|
+
|
|
902
|
+
def _identity_fun(x: Any) -> Any:
|
|
903
|
+
return x
|
|
904
|
+
|
|
905
|
+
return DiscretePalette(_identity_fun, type="numeric")
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def pal_manual(
|
|
909
|
+
values: Union[list[Any], dict[str, Any]],
|
|
910
|
+
type: str = "colour",
|
|
911
|
+
) -> DiscretePalette:
|
|
912
|
+
"""
|
|
913
|
+
Manual palette from user-supplied values.
|
|
914
|
+
|
|
915
|
+
Parameters
|
|
916
|
+
----------
|
|
917
|
+
values : list or dict
|
|
918
|
+
Palette values. If a list, the first *n* entries are returned.
|
|
919
|
+
If a dict, values are returned in insertion order.
|
|
920
|
+
type : str, optional
|
|
921
|
+
``"colour"`` (default) or ``"numeric"``.
|
|
922
|
+
|
|
923
|
+
Returns
|
|
924
|
+
-------
|
|
925
|
+
DiscretePalette
|
|
926
|
+
"""
|
|
927
|
+
if isinstance(values, dict):
|
|
928
|
+
vals = list(values.values())
|
|
929
|
+
else:
|
|
930
|
+
vals = list(values)
|
|
931
|
+
|
|
932
|
+
max_n = len(vals)
|
|
933
|
+
|
|
934
|
+
def _manual_fun(n: int) -> list[Any]:
|
|
935
|
+
# Mirrors R's pal_manual: warn (don't abort) when n exceeds the
|
|
936
|
+
# palette size, and pad tail positions with None (R's NA).
|
|
937
|
+
if n > max_n:
|
|
938
|
+
warnings.warn(
|
|
939
|
+
f"This manual palette can handle a maximum of {max_n} "
|
|
940
|
+
f"values. You have supplied {n}.",
|
|
941
|
+
stacklevel=2,
|
|
942
|
+
)
|
|
943
|
+
return [*vals, *([None] * (n - max_n))]
|
|
944
|
+
return vals[:n]
|
|
945
|
+
|
|
946
|
+
return DiscretePalette(_manual_fun, type=type, nlevels=max_n)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def pal_dichromat(name: str = "Categorical.12") -> DiscretePalette:
|
|
950
|
+
"""
|
|
951
|
+
Colorblind-safe palette from the dichromat colour schemes (Light &
|
|
952
|
+
Bartlein 2004).
|
|
953
|
+
|
|
954
|
+
Faithful port of R's ``pal_dichromat``: delegates to
|
|
955
|
+
:func:`pal_manual` with ``type="colour"`` — so requesting more
|
|
956
|
+
colours than the scheme provides produces a :class:`UserWarning`
|
|
957
|
+
and pads the tail with ``None`` (R pads with ``NA``), rather than
|
|
958
|
+
aborting.
|
|
959
|
+
|
|
960
|
+
Parameters
|
|
961
|
+
----------
|
|
962
|
+
name : str, optional
|
|
963
|
+
Scheme name. All 17 schemes from R's
|
|
964
|
+
``dichromat::colorschemes`` are supported:
|
|
965
|
+
|
|
966
|
+
``BrowntoBlue.10``, ``BrowntoBlue.12``,
|
|
967
|
+
``BluetoDarkOrange.12``, ``BluetoDarkOrange.18``,
|
|
968
|
+
``DarkRedtoBlue.12``, ``DarkRedtoBlue.18``,
|
|
969
|
+
``BluetoGreen.14``, ``BluetoGray.8``,
|
|
970
|
+
``BluetoOrangeRed.14``,
|
|
971
|
+
``BluetoOrange.8``, ``BluetoOrange.10``, ``BluetoOrange.12``,
|
|
972
|
+
``LightBluetoDarkBlue.7``, ``LightBluetoDarkBlue.10``,
|
|
973
|
+
``Categorical.12``, ``GreentoMagenta.16``,
|
|
974
|
+
``SteppedSequential.5``.
|
|
975
|
+
|
|
976
|
+
Default ``"Categorical.12"``.
|
|
977
|
+
|
|
978
|
+
Returns
|
|
979
|
+
-------
|
|
980
|
+
DiscretePalette
|
|
981
|
+
"""
|
|
982
|
+
if name not in _DICHROMAT_SCHEMES:
|
|
983
|
+
available = ", ".join(sorted(_DICHROMAT_SCHEMES))
|
|
984
|
+
raise ValueError(
|
|
985
|
+
f"Unknown dichromat scheme {name!r}. Available: {available}"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
colours = list(_DICHROMAT_SCHEMES[name])
|
|
989
|
+
# R: pal_dichromat <- pal_manual(pal, type="colour"). pal_manual's
|
|
990
|
+
# `n > length(values)` branch warns and pads with NA.
|
|
991
|
+
return pal_manual(colours, type="colour")
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
# ===================================================================
|
|
995
|
+
# Continuous palette factories
|
|
996
|
+
# ===================================================================
|
|
997
|
+
|
|
998
|
+
def pal_gradient_n(
|
|
999
|
+
colours: Sequence[str],
|
|
1000
|
+
values: Optional[Sequence[float]] = None,
|
|
1001
|
+
space: str = "Lab",
|
|
1002
|
+
) -> ContinuousPalette:
|
|
1003
|
+
"""
|
|
1004
|
+
Gradient through *n* colours, interpolated in CIELAB colour space.
|
|
1005
|
+
|
|
1006
|
+
Parameters
|
|
1007
|
+
----------
|
|
1008
|
+
colours : sequence of str
|
|
1009
|
+
Colour strings defining the gradient stops.
|
|
1010
|
+
values : sequence of float or None, optional
|
|
1011
|
+
Positions of each colour in ``[0, 1]``. If ``None``, colours are
|
|
1012
|
+
evenly spaced.
|
|
1013
|
+
space : str, optional
|
|
1014
|
+
Colour interpolation space. Must be ``"Lab"`` — other values
|
|
1015
|
+
are deprecated (matching R scales >= 0.3.0).
|
|
1016
|
+
|
|
1017
|
+
Returns
|
|
1018
|
+
-------
|
|
1019
|
+
ContinuousPalette
|
|
1020
|
+
"""
|
|
1021
|
+
from .colour_ramp import colour_ramp
|
|
1022
|
+
|
|
1023
|
+
ramp = colour_ramp(colours)
|
|
1024
|
+
|
|
1025
|
+
if values is not None:
|
|
1026
|
+
values_arr = np.asarray(values, dtype=float)
|
|
1027
|
+
if len(values_arr) != len(colours):
|
|
1028
|
+
raise ValueError(
|
|
1029
|
+
f"Length of values ({len(values_arr)}) must match "
|
|
1030
|
+
f"length of colours ({len(colours)})"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
def _gradient_fun(x: ArrayLike) -> list[str]:
|
|
1034
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1035
|
+
if x_arr.size == 0:
|
|
1036
|
+
return []
|
|
1037
|
+
if values is not None:
|
|
1038
|
+
# Remap x through the custom value positions.
|
|
1039
|
+
# R uses approxfun(values, xs) which returns NA for
|
|
1040
|
+
# extrapolation (rule=1). np.interp clamps, so we
|
|
1041
|
+
# must manually set out-of-range values to NaN.
|
|
1042
|
+
xs = np.linspace(0.0, 1.0, len(values_arr))
|
|
1043
|
+
lo, hi = values_arr[0], values_arr[-1]
|
|
1044
|
+
x_arr = np.where(
|
|
1045
|
+
(x_arr < lo) | (x_arr > hi), np.nan,
|
|
1046
|
+
np.interp(x_arr, values_arr, xs),
|
|
1047
|
+
)
|
|
1048
|
+
return ramp(x_arr)
|
|
1049
|
+
|
|
1050
|
+
return ContinuousPalette(_gradient_fun, type="colour", na_safe=False)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def pal_div_gradient(
|
|
1054
|
+
low: str = "#2B6788",
|
|
1055
|
+
mid: str = "#CBCBCB",
|
|
1056
|
+
high: str = "#90503F",
|
|
1057
|
+
space: str = "Lab",
|
|
1058
|
+
) -> ContinuousPalette:
|
|
1059
|
+
"""
|
|
1060
|
+
Diverging gradient palette.
|
|
1061
|
+
|
|
1062
|
+
Parameters
|
|
1063
|
+
----------
|
|
1064
|
+
low : str, optional
|
|
1065
|
+
Colour for the low end. Default ``"#2B6788"``.
|
|
1066
|
+
mid : str, optional
|
|
1067
|
+
Colour for the midpoint. Default ``"#CBCBCB"``.
|
|
1068
|
+
high : str, optional
|
|
1069
|
+
Colour for the high end. Default ``"#90503F"``.
|
|
1070
|
+
space : str, optional
|
|
1071
|
+
Interpolation colour space. Default ``"Lab"``.
|
|
1072
|
+
|
|
1073
|
+
Returns
|
|
1074
|
+
-------
|
|
1075
|
+
ContinuousPalette
|
|
1076
|
+
"""
|
|
1077
|
+
return pal_gradient_n([low, mid, high], values=[0.0, 0.5, 1.0], space=space)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def pal_seq_gradient(
|
|
1081
|
+
low: str = "#2B6788",
|
|
1082
|
+
high: str = "#90503F",
|
|
1083
|
+
space: str = "Lab",
|
|
1084
|
+
) -> ContinuousPalette:
|
|
1085
|
+
"""
|
|
1086
|
+
Sequential gradient palette.
|
|
1087
|
+
|
|
1088
|
+
Parameters
|
|
1089
|
+
----------
|
|
1090
|
+
low : str, optional
|
|
1091
|
+
Colour for the low end. Default ``"#2B6788"``.
|
|
1092
|
+
high : str, optional
|
|
1093
|
+
Colour for the high end. Default ``"#90503F"``.
|
|
1094
|
+
space : str, optional
|
|
1095
|
+
Interpolation colour space. Default ``"Lab"``.
|
|
1096
|
+
|
|
1097
|
+
Returns
|
|
1098
|
+
-------
|
|
1099
|
+
ContinuousPalette
|
|
1100
|
+
"""
|
|
1101
|
+
return pal_gradient_n([low, high], space=space)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def pal_area(
|
|
1105
|
+
range: Tuple[float, float] = (1, 6),
|
|
1106
|
+
) -> ContinuousPalette:
|
|
1107
|
+
"""
|
|
1108
|
+
Area-scaling palette (numeric, not colour).
|
|
1109
|
+
|
|
1110
|
+
Maps values in ``[0, 1]`` to ``sqrt``-scaled sizes in *range*.
|
|
1111
|
+
|
|
1112
|
+
Parameters
|
|
1113
|
+
----------
|
|
1114
|
+
range : tuple of float, optional
|
|
1115
|
+
``(min_size, max_size)``. Default ``(1, 6)``.
|
|
1116
|
+
|
|
1117
|
+
Returns
|
|
1118
|
+
-------
|
|
1119
|
+
ContinuousPalette
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
def _area_fun(x: ArrayLike) -> np.ndarray:
|
|
1123
|
+
x = np.asarray(x, dtype=float)
|
|
1124
|
+
return range[0] + np.sqrt(x) * (range[1] - range[0])
|
|
1125
|
+
|
|
1126
|
+
return ContinuousPalette(_area_fun, type="numeric", na_safe=False)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def pal_rescale(
|
|
1130
|
+
range: Tuple[float, float] = (0.1, 1),
|
|
1131
|
+
) -> ContinuousPalette:
|
|
1132
|
+
"""
|
|
1133
|
+
Rescale palette (numeric).
|
|
1134
|
+
|
|
1135
|
+
Linearly maps ``[0, 1]`` into *range*.
|
|
1136
|
+
|
|
1137
|
+
Parameters
|
|
1138
|
+
----------
|
|
1139
|
+
range : tuple of float, optional
|
|
1140
|
+
Target range. Default ``(0.1, 1)``.
|
|
1141
|
+
|
|
1142
|
+
Returns
|
|
1143
|
+
-------
|
|
1144
|
+
ContinuousPalette
|
|
1145
|
+
"""
|
|
1146
|
+
|
|
1147
|
+
def _rescale_fun(x: ArrayLike) -> np.ndarray:
|
|
1148
|
+
x = np.asarray(x, dtype=float)
|
|
1149
|
+
return range[0] + x * (range[1] - range[0])
|
|
1150
|
+
|
|
1151
|
+
return ContinuousPalette(_rescale_fun, type="numeric", na_safe=False)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def abs_area(max_val: float) -> ContinuousPalette:
|
|
1155
|
+
"""
|
|
1156
|
+
Absolute-area palette.
|
|
1157
|
+
|
|
1158
|
+
Maps the absolute value of input into ``[0, max_val]`` via square-root
|
|
1159
|
+
scaling, so that ``0`` always maps to size ``0``.
|
|
1160
|
+
|
|
1161
|
+
Parameters
|
|
1162
|
+
----------
|
|
1163
|
+
max_val : float
|
|
1164
|
+
Maximum output size.
|
|
1165
|
+
|
|
1166
|
+
Returns
|
|
1167
|
+
-------
|
|
1168
|
+
ContinuousPalette
|
|
1169
|
+
"""
|
|
1170
|
+
|
|
1171
|
+
def _abs_area_fun(x: ArrayLike) -> np.ndarray:
|
|
1172
|
+
x = np.asarray(x, dtype=float)
|
|
1173
|
+
return np.sqrt(np.abs(x)) * max_val
|
|
1174
|
+
|
|
1175
|
+
return ContinuousPalette(_abs_area_fun, type="numeric", na_safe=False)
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
# ===================================================================
|
|
1179
|
+
# Palette registry — port of R/palette-registry.R
|
|
1180
|
+
# ===================================================================
|
|
1181
|
+
#
|
|
1182
|
+
# R's `as_continuous_pal("viridis")` / `as_discrete_pal("Set1")` resolve
|
|
1183
|
+
# names via a global environment (`.known_palettes`) populated at load
|
|
1184
|
+
# time with viridis / Brewer / dichromat / HCL palettes plus the two
|
|
1185
|
+
# factory entries `"hue"` and `"grey"`. Python mirrors that with a
|
|
1186
|
+
# lowercase-keyed module dict populated by `_init_palettes()`.
|
|
1187
|
+
|
|
1188
|
+
_KNOWN_PALETTES: dict[str, Any] = {}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def register_palette(
|
|
1192
|
+
name: str,
|
|
1193
|
+
palette: Any,
|
|
1194
|
+
warn_conflict: bool = True,
|
|
1195
|
+
) -> None:
|
|
1196
|
+
"""Register a palette under *name* in the global registry.
|
|
1197
|
+
|
|
1198
|
+
Mirrors R's ``set_palette``. Accepts any of:
|
|
1199
|
+
|
|
1200
|
+
* A :class:`ContinuousPalette` / :class:`DiscretePalette`.
|
|
1201
|
+
* A zero-argument **factory** that returns a palette (e.g.
|
|
1202
|
+
:func:`pal_hue`).
|
|
1203
|
+
* A sequence of colour strings (wrapped via :func:`pal_manual`).
|
|
1204
|
+
|
|
1205
|
+
Parameters
|
|
1206
|
+
----------
|
|
1207
|
+
name : str
|
|
1208
|
+
Registry key (case-insensitive).
|
|
1209
|
+
palette : palette, callable, or sequence
|
|
1210
|
+
Value to store.
|
|
1211
|
+
warn_conflict : bool, optional
|
|
1212
|
+
Emit :class:`UserWarning` when overwriting (default ``True``).
|
|
1213
|
+
"""
|
|
1214
|
+
key = name.lower()
|
|
1215
|
+
if warn_conflict and key in _KNOWN_PALETTES:
|
|
1216
|
+
warnings.warn(f"Overwriting pre-existing {name!r} palette.", stacklevel=2)
|
|
1217
|
+
_KNOWN_PALETTES[key] = palette
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def palette_names() -> list[str]:
|
|
1221
|
+
"""Return the sorted list of registered palette names."""
|
|
1222
|
+
return sorted(_KNOWN_PALETTES)
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def get_palette(name: str, *args: Any, **kwargs: Any) -> Union[
|
|
1226
|
+
ContinuousPalette, DiscretePalette
|
|
1227
|
+
]:
|
|
1228
|
+
"""Look up a palette by name.
|
|
1229
|
+
|
|
1230
|
+
Mirrors R's ``get_palette``:
|
|
1231
|
+
|
|
1232
|
+
* If the registered value is already a palette, return it.
|
|
1233
|
+
* If it is a **factory** (callable but not a palette), call it with
|
|
1234
|
+
``*args, **kwargs`` and return the result.
|
|
1235
|
+
* If it is a sequence of colour strings, wrap with :func:`pal_manual`.
|
|
1236
|
+
"""
|
|
1237
|
+
key = name.lower()
|
|
1238
|
+
if key not in _KNOWN_PALETTES:
|
|
1239
|
+
raise KeyError(f"Unknown palette: {name!r}")
|
|
1240
|
+
val = _KNOWN_PALETTES[key]
|
|
1241
|
+
|
|
1242
|
+
if is_pal(val):
|
|
1243
|
+
return val
|
|
1244
|
+
if callable(val):
|
|
1245
|
+
try:
|
|
1246
|
+
made = val(*args, **kwargs)
|
|
1247
|
+
except TypeError:
|
|
1248
|
+
# Factory rejected extra args; try with no args.
|
|
1249
|
+
made = val()
|
|
1250
|
+
if is_pal(made):
|
|
1251
|
+
return made
|
|
1252
|
+
if isinstance(made, (list, tuple)):
|
|
1253
|
+
return pal_manual(list(made), type="colour")
|
|
1254
|
+
raise ValueError(
|
|
1255
|
+
f"Factory for palette {name!r} did not return a palette."
|
|
1256
|
+
)
|
|
1257
|
+
if isinstance(val, (list, tuple)):
|
|
1258
|
+
return pal_manual(list(val), type="colour")
|
|
1259
|
+
|
|
1260
|
+
raise ValueError(f"Cannot interpret registered entry for {name!r}.")
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def reset_palettes() -> None:
|
|
1264
|
+
"""Clear the registry and re-seed with the built-in R palettes."""
|
|
1265
|
+
_KNOWN_PALETTES.clear()
|
|
1266
|
+
_init_palettes()
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _init_palettes() -> None:
|
|
1270
|
+
"""Seed the registry with R's built-in palette set.
|
|
1271
|
+
|
|
1272
|
+
Mirrors R's ``init_palettes``: registers viridis, Brewer, and
|
|
1273
|
+
dichromat entries plus the factories ``"hue"`` and ``"grey"``.
|
|
1274
|
+
HCL ramps are not ported (they depend on grDevices internals).
|
|
1275
|
+
"""
|
|
1276
|
+
from ._palettes_data import BREWER, VIRIDIS
|
|
1277
|
+
|
|
1278
|
+
for name in VIRIDIS:
|
|
1279
|
+
register_palette(name, pal_viridis(option=name), warn_conflict=False)
|
|
1280
|
+
|
|
1281
|
+
# R's viridis lets users pass either the full name or the letter
|
|
1282
|
+
# code ("D" for "viridis", "A" for "magma", ...). Mirror that
|
|
1283
|
+
# convenience by registering each letter alias.
|
|
1284
|
+
_VIRIDIS_LETTER_ALIASES = {
|
|
1285
|
+
"A": "magma", "B": "inferno", "C": "plasma", "D": "viridis",
|
|
1286
|
+
"E": "cividis", "F": "rocket", "G": "mako", "H": "turbo",
|
|
1287
|
+
}
|
|
1288
|
+
for letter, full_name in _VIRIDIS_LETTER_ALIASES.items():
|
|
1289
|
+
if full_name in VIRIDIS:
|
|
1290
|
+
register_palette(
|
|
1291
|
+
letter, pal_viridis(option=full_name), warn_conflict=False
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
for name in BREWER:
|
|
1295
|
+
register_palette(name, pal_brewer(palette=name), warn_conflict=False)
|
|
1296
|
+
|
|
1297
|
+
for name in _DICHROMAT_SCHEMES:
|
|
1298
|
+
register_palette(
|
|
1299
|
+
name, pal_dichromat(name=name), warn_conflict=False
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# R registers pal_hue / pal_grey as factories so users can override
|
|
1303
|
+
# parameters via `get_palette("hue", h=c(0,90))`.
|
|
1304
|
+
register_palette("hue", pal_hue, warn_conflict=False)
|
|
1305
|
+
register_palette("grey", pal_grey, warn_conflict=False)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
_init_palettes()
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
# ===================================================================
|
|
1312
|
+
# Legacy aliases
|
|
1313
|
+
# ===================================================================
|
|
1314
|
+
|
|
1315
|
+
brewer_pal = pal_brewer
|
|
1316
|
+
hue_pal = pal_hue
|
|
1317
|
+
viridis_pal = pal_viridis
|
|
1318
|
+
grey_pal = pal_grey
|
|
1319
|
+
shape_pal = pal_shape
|
|
1320
|
+
linetype_pal = pal_linetype
|
|
1321
|
+
identity_pal = pal_identity
|
|
1322
|
+
manual_pal = pal_manual
|
|
1323
|
+
dichromat_pal = pal_dichromat
|
|
1324
|
+
gradient_n_pal = pal_gradient_n
|
|
1325
|
+
div_gradient_pal = pal_div_gradient
|
|
1326
|
+
seq_gradient_pal = pal_seq_gradient
|
|
1327
|
+
area_pal = pal_area
|
|
1328
|
+
rescale_pal = pal_rescale
|