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/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