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/colour_manip.py
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Colour manipulation utilities for the scales package.
|
|
3
|
+
|
|
4
|
+
Python port of R/colour-manip.R from the R scales package
|
|
5
|
+
(https://github.com/r-lib/scales). Provides helpers for adjusting
|
|
6
|
+
transparency, desaturation, HCL manipulation, mixing, and display.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ._colors import to_hex, to_rgb, to_rgba
|
|
16
|
+
from numpy.typing import ArrayLike
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"alpha",
|
|
20
|
+
"muted",
|
|
21
|
+
"col2hcl",
|
|
22
|
+
"show_col",
|
|
23
|
+
"col_mix",
|
|
24
|
+
"col_shift",
|
|
25
|
+
"col_lighter",
|
|
26
|
+
"col_darker",
|
|
27
|
+
"col_saturate",
|
|
28
|
+
"wrap_col_adjustment",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Internal colour-space helpers
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _linearize(c: float) -> float:
|
|
37
|
+
"""sRGB channel [0, 1] -> linear RGB."""
|
|
38
|
+
if c <= 0.04045:
|
|
39
|
+
return c / 12.92
|
|
40
|
+
return ((c + 0.055) / 1.055) ** 2.4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _delinearize(c: float) -> float:
|
|
44
|
+
"""Linear RGB -> sRGB channel [0, 1]."""
|
|
45
|
+
if c <= 0.0031308:
|
|
46
|
+
return 12.92 * c
|
|
47
|
+
return 1.055 * (c ** (1.0 / 2.4)) - 0.055
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _rgb_to_xyz(r: float, g: float, b: float) -> tuple[float, float, float]:
|
|
51
|
+
"""sRGB [0, 1] -> CIE XYZ (D65)."""
|
|
52
|
+
rl = _linearize(r)
|
|
53
|
+
gl = _linearize(g)
|
|
54
|
+
bl = _linearize(b)
|
|
55
|
+
x = 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl
|
|
56
|
+
y = 0.2126729 * rl + 0.7151522 * gl + 0.0721750 * bl
|
|
57
|
+
z = 0.0193339 * rl + 0.1191920 * gl + 0.9503041 * bl
|
|
58
|
+
return x, y, z
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _xyz_to_rgb(x: float, y: float, z: float) -> tuple[float, float, float]:
|
|
62
|
+
"""CIE XYZ (D65) -> sRGB [0, 1], clamped."""
|
|
63
|
+
rl = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
|
|
64
|
+
gl = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
|
|
65
|
+
bl = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
|
|
66
|
+
r = np.clip(_delinearize(rl), 0.0, 1.0)
|
|
67
|
+
g = np.clip(_delinearize(gl), 0.0, 1.0)
|
|
68
|
+
b = np.clip(_delinearize(bl), 0.0, 1.0)
|
|
69
|
+
return float(r), float(g), float(b)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _rgb_to_lab(r: float, g: float, b: float) -> tuple[float, float, float]:
|
|
73
|
+
"""
|
|
74
|
+
Convert sRGB [0, 1] to CIELAB (D65 illuminant).
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
r, g, b : float
|
|
79
|
+
sRGB colour channels in [0, 1].
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
tuple of float
|
|
84
|
+
``(L, a, b)`` in CIELAB space.
|
|
85
|
+
"""
|
|
86
|
+
# sRGB -> linear
|
|
87
|
+
rl = _linearize(r)
|
|
88
|
+
gl = _linearize(g)
|
|
89
|
+
bl = _linearize(b)
|
|
90
|
+
|
|
91
|
+
# linear RGB -> XYZ (D65)
|
|
92
|
+
x = 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl
|
|
93
|
+
y = 0.2126729 * rl + 0.7151522 * gl + 0.0721750 * bl
|
|
94
|
+
z = 0.0193339 * rl + 0.1191920 * gl + 0.9503041 * bl
|
|
95
|
+
|
|
96
|
+
# D65 reference white
|
|
97
|
+
xn, yn, zn = 0.95047, 1.0, 1.08883
|
|
98
|
+
|
|
99
|
+
def _f(t: float) -> float:
|
|
100
|
+
delta = 6.0 / 29.0
|
|
101
|
+
if t > delta ** 3:
|
|
102
|
+
return t ** (1.0 / 3.0)
|
|
103
|
+
return t / (3.0 * delta * delta) + 4.0 / 29.0
|
|
104
|
+
|
|
105
|
+
fx = _f(x / xn)
|
|
106
|
+
fy = _f(y / yn)
|
|
107
|
+
fz = _f(z / zn)
|
|
108
|
+
|
|
109
|
+
L = 116.0 * fy - 16.0
|
|
110
|
+
a_star = 500.0 * (fx - fy)
|
|
111
|
+
b_star = 200.0 * (fy - fz)
|
|
112
|
+
return L, a_star, b_star
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _lab_to_rgb(
|
|
116
|
+
L: float, a_star: float, b_star: float
|
|
117
|
+
) -> tuple[float, float, float]:
|
|
118
|
+
"""
|
|
119
|
+
Convert CIELAB (D65) to sRGB [0, 1], clamped.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
L, a_star, b_star : float
|
|
124
|
+
CIELAB coordinates.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
tuple of float
|
|
129
|
+
``(r, g, b)`` clamped to [0, 1].
|
|
130
|
+
"""
|
|
131
|
+
xn, yn, zn = 0.95047, 1.0, 1.08883
|
|
132
|
+
delta = 6.0 / 29.0
|
|
133
|
+
|
|
134
|
+
fy = (L + 16.0) / 116.0
|
|
135
|
+
fx = fy + a_star / 500.0
|
|
136
|
+
fz = fy - b_star / 200.0
|
|
137
|
+
|
|
138
|
+
def _finv(t: float) -> float:
|
|
139
|
+
if t > delta:
|
|
140
|
+
return t ** 3
|
|
141
|
+
return 3.0 * delta * delta * (t - 4.0 / 29.0)
|
|
142
|
+
|
|
143
|
+
x = xn * _finv(fx)
|
|
144
|
+
y = yn * _finv(fy)
|
|
145
|
+
z = zn * _finv(fz)
|
|
146
|
+
|
|
147
|
+
# XYZ -> linear RGB
|
|
148
|
+
rl = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
|
|
149
|
+
gl = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
|
|
150
|
+
bl = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
|
|
151
|
+
|
|
152
|
+
r = np.clip(_delinearize(rl), 0.0, 1.0)
|
|
153
|
+
g = np.clip(_delinearize(gl), 0.0, 1.0)
|
|
154
|
+
b = np.clip(_delinearize(bl), 0.0, 1.0)
|
|
155
|
+
return float(r), float(g), float(b)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _lab_to_hcl(
|
|
159
|
+
L: float, a: float, b: float
|
|
160
|
+
) -> tuple[float, float, float]:
|
|
161
|
+
"""
|
|
162
|
+
Convert CIELAB to HCL (cylindrical Lab).
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
tuple of float
|
|
167
|
+
``(H, C, L)`` where H is in degrees [0, 360).
|
|
168
|
+
"""
|
|
169
|
+
h = np.degrees(np.arctan2(b, a)) % 360
|
|
170
|
+
c = np.sqrt(a ** 2 + b ** 2)
|
|
171
|
+
return float(h), float(c), L
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _hcl_to_lab(
|
|
175
|
+
h: float, c: float, l: float
|
|
176
|
+
) -> tuple[float, float, float]:
|
|
177
|
+
"""
|
|
178
|
+
Convert HCL back to CIELAB.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
h : float
|
|
183
|
+
Hue in degrees.
|
|
184
|
+
c : float
|
|
185
|
+
Chroma.
|
|
186
|
+
l : float
|
|
187
|
+
Luminance (CIELAB *L*).
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
tuple of float
|
|
192
|
+
``(L, a, b)`` in CIELAB.
|
|
193
|
+
"""
|
|
194
|
+
a = c * np.cos(np.radians(h))
|
|
195
|
+
b = c * np.sin(np.radians(h))
|
|
196
|
+
return l, float(a), float(b)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _hex_to_hcl(colour: str) -> tuple[float, float, float, float]:
|
|
200
|
+
"""Return ``(H, C, L, alpha)`` for a colour string."""
|
|
201
|
+
rgba = to_rgba(colour)
|
|
202
|
+
L, a, b = _rgb_to_lab(rgba[0], rgba[1], rgba[2])
|
|
203
|
+
h, c, l = _lab_to_hcl(L, a, b)
|
|
204
|
+
return h, c, l, rgba[3]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _hcl_to_hex(
|
|
208
|
+
h: float, c: float, l: float, alpha_val: float = 1.0
|
|
209
|
+
) -> str:
|
|
210
|
+
"""Return hex string from HCL + alpha."""
|
|
211
|
+
L, a, b = _hcl_to_lab(h, c, l)
|
|
212
|
+
r, g, bb = _lab_to_rgb(L, a, b)
|
|
213
|
+
return to_hex((r, g, bb, alpha_val), keep_alpha=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Public API
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def alpha(
|
|
221
|
+
colour: Union[str, Sequence[str]],
|
|
222
|
+
alpha_value: Union[None, float, Sequence[Optional[float]]] = None,
|
|
223
|
+
) -> Union[str, List[str]]:
|
|
224
|
+
"""
|
|
225
|
+
Modify the alpha transparency of colour(s).
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
colour : str or sequence of str
|
|
230
|
+
One or more colour specifications (names, hex codes, etc.).
|
|
231
|
+
alpha_value : float or sequence of float, optional
|
|
232
|
+
New alpha value(s) in [0, 1]. If *None*, colours are returned
|
|
233
|
+
unchanged.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
str or list of str
|
|
238
|
+
Colour(s) with the requested alpha channel, as ``#RRGGBBAA`` hex
|
|
239
|
+
strings.
|
|
240
|
+
|
|
241
|
+
Examples
|
|
242
|
+
--------
|
|
243
|
+
>>> alpha("red", 0.5)
|
|
244
|
+
'#ff000080'
|
|
245
|
+
"""
|
|
246
|
+
scalar_input = isinstance(colour, str)
|
|
247
|
+
colours = [colour] if scalar_input else list(colour)
|
|
248
|
+
|
|
249
|
+
if alpha_value is None:
|
|
250
|
+
result = [to_hex(to_rgba(c), keep_alpha=True) for c in colours]
|
|
251
|
+
return result[0] if scalar_input else result
|
|
252
|
+
|
|
253
|
+
# Vectorise alpha_value
|
|
254
|
+
if isinstance(alpha_value, (int, float)):
|
|
255
|
+
alphas: List[Optional[float]] = [float(alpha_value)] * len(colours)
|
|
256
|
+
else:
|
|
257
|
+
alphas = list(alpha_value)
|
|
258
|
+
|
|
259
|
+
if len(alphas) == 1:
|
|
260
|
+
alphas = alphas * len(colours)
|
|
261
|
+
elif len(colours) == 1:
|
|
262
|
+
colours = colours * len(alphas)
|
|
263
|
+
|
|
264
|
+
if len(colours) != len(alphas):
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"colour and alpha_value must have compatible lengths."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
result = []
|
|
270
|
+
for c, a in zip(colours, alphas):
|
|
271
|
+
rgba = to_rgba(c)
|
|
272
|
+
# R semantics: NA alpha keeps the original colour's alpha
|
|
273
|
+
if a is None or (isinstance(a, float) and np.isnan(a)):
|
|
274
|
+
new_alpha = rgba[3]
|
|
275
|
+
else:
|
|
276
|
+
new_alpha = float(a)
|
|
277
|
+
# R's farver::encode_colour always includes alpha when provided,
|
|
278
|
+
# so force #RRGGBBAA format even for fully opaque.
|
|
279
|
+
r = int(round(rgba[0] * 255))
|
|
280
|
+
g = int(round(rgba[1] * 255))
|
|
281
|
+
b = int(round(rgba[2] * 255))
|
|
282
|
+
aa = int(round(new_alpha * 255))
|
|
283
|
+
result.append(f"#{r:02x}{g:02x}{b:02x}{aa:02x}")
|
|
284
|
+
|
|
285
|
+
return result[0] if scalar_input and isinstance(alpha_value, (int, float)) else result
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def muted(colour: str, l: float = 30, c: float = 70) -> str:
|
|
289
|
+
"""
|
|
290
|
+
Desaturate a colour by reducing luminance and chroma in HCL space.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
colour : str
|
|
295
|
+
A colour specification.
|
|
296
|
+
l : float, default 30
|
|
297
|
+
Target luminance (CIELAB *L*).
|
|
298
|
+
c : float, default 70
|
|
299
|
+
Target chroma.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
str
|
|
304
|
+
Muted colour as a hex string.
|
|
305
|
+
|
|
306
|
+
Examples
|
|
307
|
+
--------
|
|
308
|
+
>>> muted("red") # doctest: +SKIP
|
|
309
|
+
'#c66565ff'
|
|
310
|
+
"""
|
|
311
|
+
h_val, _c, _l, a_val = _hex_to_hcl(colour)
|
|
312
|
+
return _hcl_to_hex(h_val, c, l, a_val)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def col2hcl(
|
|
316
|
+
colour: Union[str, Sequence[str]],
|
|
317
|
+
h: Optional[float] = None,
|
|
318
|
+
c: Optional[float] = None,
|
|
319
|
+
l: Optional[float] = None,
|
|
320
|
+
alpha_value: Optional[float] = None,
|
|
321
|
+
) -> Union[str, List[str]]:
|
|
322
|
+
"""
|
|
323
|
+
Convert colour(s) to HCL, optionally overriding components, and return hex.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
colour : str or sequence of str
|
|
328
|
+
Input colour(s).
|
|
329
|
+
h, c, l : float, optional
|
|
330
|
+
Override hue, chroma, and/or luminance.
|
|
331
|
+
alpha_value : float, optional
|
|
332
|
+
Override alpha.
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
str or list of str
|
|
337
|
+
Hex colour string(s).
|
|
338
|
+
"""
|
|
339
|
+
scalar_input = isinstance(colour, str)
|
|
340
|
+
colours = [colour] if scalar_input else list(colour)
|
|
341
|
+
|
|
342
|
+
result = []
|
|
343
|
+
for col in colours:
|
|
344
|
+
hv, cv, lv, av = _hex_to_hcl(col)
|
|
345
|
+
hv = h if h is not None else hv
|
|
346
|
+
cv = c if c is not None else cv
|
|
347
|
+
lv = l if l is not None else lv
|
|
348
|
+
av = alpha_value if alpha_value is not None else av
|
|
349
|
+
result.append(_hcl_to_hex(hv, cv, lv, av))
|
|
350
|
+
|
|
351
|
+
return result[0] if scalar_input else result
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def show_col(
|
|
355
|
+
colours: Sequence[str],
|
|
356
|
+
labels: bool = True,
|
|
357
|
+
borders: Optional[str] = None,
|
|
358
|
+
cex_label: float = 1.0,
|
|
359
|
+
ncol: Optional[int] = None,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""
|
|
362
|
+
Display colours in a rectangular grid using matplotlib.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
colours : sequence of str
|
|
367
|
+
Colour specifications to display.
|
|
368
|
+
labels : bool, default True
|
|
369
|
+
Whether to print the hex code inside each swatch.
|
|
370
|
+
borders : str, optional
|
|
371
|
+
Border colour for each swatch. *None* means no visible border.
|
|
372
|
+
cex_label : float, default 1.0
|
|
373
|
+
Relative text size for labels.
|
|
374
|
+
ncol : int, optional
|
|
375
|
+
Number of columns. If *None*, a roughly square layout is chosen.
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
import matplotlib.pyplot as plt
|
|
379
|
+
except ImportError:
|
|
380
|
+
raise ImportError(
|
|
381
|
+
"show_col() requires matplotlib. Install with: "
|
|
382
|
+
"pip install scales_py[plot]"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
n = len(colours)
|
|
386
|
+
if n == 0:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
if ncol is None:
|
|
390
|
+
ncol = int(np.ceil(np.sqrt(n)))
|
|
391
|
+
nrow = int(np.ceil(n / ncol))
|
|
392
|
+
|
|
393
|
+
fig, axes = plt.subplots(
|
|
394
|
+
nrow, ncol, figsize=(ncol * 1.2, nrow * 1.2),
|
|
395
|
+
squeeze=False,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
for idx in range(nrow * ncol):
|
|
399
|
+
row, col_idx = divmod(idx, ncol)
|
|
400
|
+
ax = axes[row][col_idx]
|
|
401
|
+
ax.set_xticks([])
|
|
402
|
+
ax.set_yticks([])
|
|
403
|
+
|
|
404
|
+
if idx < n:
|
|
405
|
+
ax.set_facecolor(colours[idx])
|
|
406
|
+
edge = borders if borders is not None else "none"
|
|
407
|
+
for spine in ax.spines.values():
|
|
408
|
+
spine.set_edgecolor(edge)
|
|
409
|
+
spine.set_linewidth(1 if borders else 0)
|
|
410
|
+
if labels:
|
|
411
|
+
r, g, b = to_rgb(colours[idx])
|
|
412
|
+
lum = 0.299 * r + 0.587 * g + 0.114 * b
|
|
413
|
+
text_col = "white" if lum < 0.5 else "black"
|
|
414
|
+
ax.text(
|
|
415
|
+
0.5, 0.5,
|
|
416
|
+
to_hex(colours[idx]),
|
|
417
|
+
transform=ax.transAxes,
|
|
418
|
+
ha="center", va="center",
|
|
419
|
+
fontsize=8 * cex_label,
|
|
420
|
+
color=text_col,
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
ax.set_visible(False)
|
|
424
|
+
|
|
425
|
+
fig.tight_layout(pad=0.5)
|
|
426
|
+
plt.show()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
_COL_MIX_SPACES = {"rgb", "lab", "hcl", "lch", "hsl", "xyz"}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def wrap_col_adjustment(
|
|
433
|
+
inner: object,
|
|
434
|
+
outer_fn,
|
|
435
|
+
/,
|
|
436
|
+
**outer_kwargs,
|
|
437
|
+
):
|
|
438
|
+
"""Wrap a colour palette so *outer_fn* is applied to each output colour.
|
|
439
|
+
|
|
440
|
+
Port of R's ``wrap_col_adjustment`` (``R/colour-manip.R``). Used by
|
|
441
|
+
``col_mix``/``col_shift``/``col_lighter``/``col_saturate`` when the
|
|
442
|
+
first argument is a palette rather than a bare colour string — the
|
|
443
|
+
result is a **new palette** of the same kind whose outputs are the
|
|
444
|
+
adjusted colours.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
inner : ContinuousPalette or DiscretePalette
|
|
449
|
+
The colour palette to wrap. Must be a colour palette
|
|
450
|
+
(``palette_type(inner) == "colour"``).
|
|
451
|
+
outer_fn : callable
|
|
452
|
+
One of ``col_mix`` / ``col_shift`` / ``col_lighter`` / ``col_darker``
|
|
453
|
+
/ ``col_saturate`` — called as
|
|
454
|
+
``outer_fn(inner(...), **outer_kwargs)``.
|
|
455
|
+
**outer_kwargs
|
|
456
|
+
Passed to *outer_fn* on each invocation.
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
ContinuousPalette or DiscretePalette
|
|
461
|
+
A new palette, same kind as *inner*, returning adjusted colours.
|
|
462
|
+
"""
|
|
463
|
+
from .palettes import (
|
|
464
|
+
ContinuousPalette,
|
|
465
|
+
DiscretePalette,
|
|
466
|
+
is_colour_pal,
|
|
467
|
+
palette_nlevels,
|
|
468
|
+
palette_na_safe,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if not is_colour_pal(inner):
|
|
472
|
+
raise TypeError("wrap_col_adjustment requires a colour palette")
|
|
473
|
+
|
|
474
|
+
def _adjusted(*args, **kwargs):
|
|
475
|
+
raw = inner(*args, **kwargs)
|
|
476
|
+
if isinstance(raw, str):
|
|
477
|
+
return outer_fn(raw, **outer_kwargs)
|
|
478
|
+
# list / tuple / ndarray of colours
|
|
479
|
+
return [outer_fn(c, **outer_kwargs) for c in raw]
|
|
480
|
+
|
|
481
|
+
if isinstance(inner, DiscretePalette):
|
|
482
|
+
return DiscretePalette(
|
|
483
|
+
_adjusted, type="colour", nlevels=palette_nlevels(inner)
|
|
484
|
+
)
|
|
485
|
+
return ContinuousPalette(
|
|
486
|
+
_adjusted, type="colour", na_safe=palette_na_safe(inner)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _is_palette_input(x) -> bool:
|
|
491
|
+
"""True if *x* is a scales palette object (discrete or continuous)."""
|
|
492
|
+
from .palettes import ContinuousPalette, DiscretePalette
|
|
493
|
+
return isinstance(x, (ContinuousPalette, DiscretePalette))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def col_mix(
|
|
497
|
+
a: str,
|
|
498
|
+
b: str,
|
|
499
|
+
amount: float = 0.5,
|
|
500
|
+
space: str = "rgb",
|
|
501
|
+
) -> str:
|
|
502
|
+
"""
|
|
503
|
+
Mix two colours.
|
|
504
|
+
|
|
505
|
+
Mirrors R's ``col_mix``: components are interpolated linearly in the
|
|
506
|
+
requested *space*. Hue channels are interpolated as plain numbers
|
|
507
|
+
(not circular shortest-path), matching ``farver``'s behaviour.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
a, b : str
|
|
512
|
+
Colour specifications.
|
|
513
|
+
amount : float, default 0.5
|
|
514
|
+
Mixing fraction. 0 returns *a*, 1 returns *b*.
|
|
515
|
+
space : str, default "rgb"
|
|
516
|
+
Interpolation space. One of ``"rgb"``, ``"lab"``, ``"hcl"``,
|
|
517
|
+
``"lch"`` (alias of ``"hcl"``), or ``"hsl"``.
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
str
|
|
522
|
+
Mixed colour as a hex string.
|
|
523
|
+
"""
|
|
524
|
+
if not (0.0 <= amount <= 1.0):
|
|
525
|
+
raise ValueError(f"amount must be between 0 and 1, got {amount}")
|
|
526
|
+
if space not in _COL_MIX_SPACES:
|
|
527
|
+
raise ValueError(
|
|
528
|
+
f"space must be one of {sorted(_COL_MIX_SPACES)!r}, got {space!r}"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# S3-style dispatch on palette first arg (mirrors R's
|
|
532
|
+
# `col_mix.scales_pal` → `wrap_col_adjustment`).
|
|
533
|
+
if _is_palette_input(a):
|
|
534
|
+
return wrap_col_adjustment(a, col_mix, b=b, amount=amount, space=space)
|
|
535
|
+
|
|
536
|
+
rgba_a = to_rgba(a)
|
|
537
|
+
rgba_b = to_rgba(b)
|
|
538
|
+
alpha_m = rgba_a[3] + amount * (rgba_b[3] - rgba_a[3])
|
|
539
|
+
|
|
540
|
+
if space == "rgb":
|
|
541
|
+
mixed = tuple(
|
|
542
|
+
rgba_a[i] + amount * (rgba_b[i] - rgba_a[i]) for i in range(4)
|
|
543
|
+
)
|
|
544
|
+
return to_hex(mixed, keep_alpha=True)
|
|
545
|
+
|
|
546
|
+
if space == "lab":
|
|
547
|
+
La, aa, ba = _rgb_to_lab(*rgba_a[:3])
|
|
548
|
+
Lb, ab, bb = _rgb_to_lab(*rgba_b[:3])
|
|
549
|
+
Lm = La + amount * (Lb - La)
|
|
550
|
+
am = aa + amount * (ab - aa)
|
|
551
|
+
bm = ba + amount * (bb - ba)
|
|
552
|
+
r, g, bl = _lab_to_rgb(Lm, am, bm)
|
|
553
|
+
return to_hex((r, g, bl, alpha_m), keep_alpha=True)
|
|
554
|
+
|
|
555
|
+
if space in ("hcl", "lch"):
|
|
556
|
+
# HCL == LCH (cylindrical CIELAB), farver-style linear hue mix.
|
|
557
|
+
La, aa, ba = _rgb_to_lab(*rgba_a[:3])
|
|
558
|
+
Lb, ab, bb = _rgb_to_lab(*rgba_b[:3])
|
|
559
|
+
ha, ca, la_ = _lab_to_hcl(La, aa, ba)
|
|
560
|
+
hb, cb, lb_ = _lab_to_hcl(Lb, ab, bb)
|
|
561
|
+
hm = ha + amount * (hb - ha)
|
|
562
|
+
cm = ca + amount * (cb - ca)
|
|
563
|
+
lm = la_ + amount * (lb_ - la_)
|
|
564
|
+
L, a_, b_ = _hcl_to_lab(hm, cm, lm)
|
|
565
|
+
r, g, bl = _lab_to_rgb(L, a_, b_)
|
|
566
|
+
return to_hex((r, g, bl, alpha_m), keep_alpha=True)
|
|
567
|
+
|
|
568
|
+
if space == "xyz":
|
|
569
|
+
xa, ya, za = _rgb_to_xyz(*rgba_a[:3])
|
|
570
|
+
xb, yb, zb = _rgb_to_xyz(*rgba_b[:3])
|
|
571
|
+
xm = xa + amount * (xb - xa)
|
|
572
|
+
ym = ya + amount * (yb - ya)
|
|
573
|
+
zm = za + amount * (zb - za)
|
|
574
|
+
r, g, bl = _xyz_to_rgb(xm, ym, zm)
|
|
575
|
+
return to_hex((r, g, bl, alpha_m), keep_alpha=True)
|
|
576
|
+
|
|
577
|
+
# HSL via colorsys (which calls it HLS — same space, just channel
|
|
578
|
+
# ordering H/L/S instead of H/S/L).
|
|
579
|
+
import colorsys
|
|
580
|
+
ha, la_, sa = colorsys.rgb_to_hls(*rgba_a[:3])
|
|
581
|
+
hb, lb_, sb = colorsys.rgb_to_hls(*rgba_b[:3])
|
|
582
|
+
hm = ha + amount * (hb - ha)
|
|
583
|
+
lm = la_ + amount * (lb_ - la_)
|
|
584
|
+
sm = sa + amount * (sb - sa)
|
|
585
|
+
r, g, bl = colorsys.hls_to_rgb(hm, lm, sm)
|
|
586
|
+
return to_hex((r, g, bl, alpha_m), keep_alpha=True)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def col_shift(col, amount: float = 10):
|
|
590
|
+
"""
|
|
591
|
+
Shift the hue of a colour.
|
|
592
|
+
|
|
593
|
+
Accepts a colour string **or** a colour palette. When a palette is
|
|
594
|
+
passed, returns a new palette whose colours are all hue-shifted by
|
|
595
|
+
*amount* (mirrors R's ``col_shift.scales_pal``).
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
col : str or palette
|
|
600
|
+
A colour specification or a scales palette object.
|
|
601
|
+
amount : float, default 10
|
|
602
|
+
Degrees to shift hue.
|
|
603
|
+
|
|
604
|
+
Returns
|
|
605
|
+
-------
|
|
606
|
+
str or palette
|
|
607
|
+
Hue-shifted colour(s).
|
|
608
|
+
"""
|
|
609
|
+
if _is_palette_input(col):
|
|
610
|
+
return wrap_col_adjustment(col, col_shift, amount=amount)
|
|
611
|
+
h, c, l, a = _hex_to_hcl(col)
|
|
612
|
+
return _hcl_to_hex((h + amount) % 360, c, l, a)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def col_lighter(col, amount: float = 10):
|
|
616
|
+
"""
|
|
617
|
+
Increase the lightness of a colour in HSL space.
|
|
618
|
+
|
|
619
|
+
Matches R's ``farver::add_to_channel(col, "l", amount, space = "hsl")``.
|
|
620
|
+
Accepts a colour string or a palette (wrapped via
|
|
621
|
+
:func:`wrap_col_adjustment`).
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
col : str or palette
|
|
626
|
+
A colour specification or a scales palette object.
|
|
627
|
+
amount : float, default 10
|
|
628
|
+
Amount to add to lightness (HSL *L*, range 0–100).
|
|
629
|
+
|
|
630
|
+
Returns
|
|
631
|
+
-------
|
|
632
|
+
str or palette
|
|
633
|
+
"""
|
|
634
|
+
if _is_palette_input(col):
|
|
635
|
+
return wrap_col_adjustment(col, col_lighter, amount=amount)
|
|
636
|
+
import colorsys
|
|
637
|
+
rgba = to_rgba(col)
|
|
638
|
+
h, l, s = colorsys.rgb_to_hls(rgba[0], rgba[1], rgba[2])
|
|
639
|
+
# HSL L is [0,1], R uses [0,100] scale for amount
|
|
640
|
+
l = max(0.0, min(1.0, l + amount / 100.0))
|
|
641
|
+
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
|
642
|
+
return to_hex((r, g, b, rgba[3]), keep_alpha=True)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def col_darker(col, amount: float = 10):
|
|
646
|
+
"""
|
|
647
|
+
Decrease the luminance of a colour.
|
|
648
|
+
|
|
649
|
+
Equivalent to ``col_lighter(col, -amount)`` — and, for palette
|
|
650
|
+
inputs, returns a palette whose colours are darkened.
|
|
651
|
+
"""
|
|
652
|
+
return col_lighter(col, -amount)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def col_saturate(col, amount: float = 10):
|
|
656
|
+
"""
|
|
657
|
+
Increase the saturation of a colour in HSL space.
|
|
658
|
+
|
|
659
|
+
Matches R's ``farver::add_to_channel(col, "s", amount, space = "hsl")``.
|
|
660
|
+
Accepts a colour string or a palette.
|
|
661
|
+
|
|
662
|
+
Parameters
|
|
663
|
+
----------
|
|
664
|
+
col : str or palette
|
|
665
|
+
A colour specification or a scales palette object.
|
|
666
|
+
amount : float, default 10
|
|
667
|
+
Amount to add to saturation (HSL *S*, range 0–100).
|
|
668
|
+
|
|
669
|
+
Returns
|
|
670
|
+
-------
|
|
671
|
+
str or palette
|
|
672
|
+
"""
|
|
673
|
+
if _is_palette_input(col):
|
|
674
|
+
return wrap_col_adjustment(col, col_saturate, amount=amount)
|
|
675
|
+
import colorsys
|
|
676
|
+
rgba = to_rgba(col)
|
|
677
|
+
h, l, s = colorsys.rgb_to_hls(rgba[0], rgba[1], rgba[2])
|
|
678
|
+
# HSL S is [0,1], R uses [0,100] scale for amount
|
|
679
|
+
s = max(0.0, min(1.0, s + amount / 100.0))
|
|
680
|
+
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
|
681
|
+
return to_hex((r, g, b, rgba[3]), keep_alpha=True)
|