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/colour_ramp.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ Colour ramp interpolation for the scales package.
3
+
4
+ Python port of R/colour-ramp.R from the R scales package
5
+ (https://github.com/r-lib/scales). Creates callable colour ramps that
6
+ map values in [0, 1] to hex colour strings by interpolation in CIELAB
7
+ colour space, matching R's ``farver``-based implementation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Callable, List, Optional, Sequence, Union
13
+
14
+ import numpy as np
15
+ from ._colors import to_hex, to_rgba
16
+ from numpy.typing import ArrayLike
17
+
18
+ from .colour_manip import _lab_to_rgb, _rgb_to_lab
19
+ from .palettes import ContinuousPalette
20
+
21
+ __all__ = [
22
+ "colour_ramp",
23
+ ]
24
+
25
+
26
+ def colour_ramp(
27
+ colors: Sequence[str],
28
+ na_color: Optional[str] = None,
29
+ alpha: bool = True,
30
+ ) -> ContinuousPalette:
31
+ """
32
+ Create a colour ramp that maps [0, 1] values to colours.
33
+
34
+ Interpolation is performed in CIELAB colour space by linearly
35
+ interpolating L, a, b channels independently (matching R's
36
+ ``farver``-based ``colour_ramp``).
37
+
38
+ Parameters
39
+ ----------
40
+ colors : sequence of str
41
+ One or more colours (any format accepted by matplotlib) that
42
+ define the endpoints and optional interior knots of the ramp.
43
+ na_color : str, optional
44
+ Colour to use for ``NaN`` / missing values. If *None*, ``NaN``
45
+ inputs produce ``None`` entries in the output list.
46
+ alpha : bool, default True
47
+ If *True*, the alpha channel is preserved in the output hex strings
48
+ (``#RRGGBBAA``). If *False*, the alpha channel is stripped.
49
+
50
+ Returns
51
+ -------
52
+ ContinuousPalette
53
+ A palette ``f(x)`` where *x* is an array-like of floats in
54
+ [0, 1]. Returns a list of hex colour strings of the same length.
55
+
56
+ Examples
57
+ --------
58
+ >>> ramp = colour_ramp(["red", "blue"])
59
+ >>> ramp([0.0, 0.5, 1.0]) # doctest: +SKIP
60
+ ['#ff0000ff', '#ca2883ff', '#0000ffff']
61
+ """
62
+ if len(colors) == 0:
63
+ raise ValueError("colour_ramp requires at least one colour.")
64
+
65
+ # Single colour: return constant function (matches R behaviour)
66
+ if len(colors) == 1:
67
+ single = to_hex(to_rgba(colors[0]), keep_alpha=True)
68
+ single_no_alpha = to_hex(to_rgba(colors[0])[:3], keep_alpha=False)
69
+
70
+ def _const_ramp(x: ArrayLike) -> List[Optional[str]]:
71
+ x = np.asarray(x, dtype=float)
72
+ result: List[Optional[str]] = []
73
+ for val in x.flat:
74
+ if np.isnan(val):
75
+ result.append(na_color)
76
+ else:
77
+ result.append(single if alpha else single_no_alpha)
78
+ return result
79
+
80
+ # R: new_continuous_palette(fun, type="colour", na_safe=!is.na(na.color))
81
+ return ContinuousPalette(
82
+ _const_ramp, type="colour", na_safe=na_color is not None
83
+ )
84
+
85
+ # Convert all colours to CIELAB + alpha
86
+ rgba_list = [to_rgba(c) for c in colors]
87
+ lab_array = np.array(
88
+ [_rgb_to_lab(r, g, b) for r, g, b, _a in rgba_list]
89
+ ) # shape (n, 3): L, a, b
90
+ alpha_array = np.array([a for _r, _g, _b, a in rgba_list])
91
+
92
+ x_in = np.linspace(0.0, 1.0, len(colors))
93
+
94
+ # R: if (!alpha || all(lab_in[, 4] == 1)) alpha_interp returns NULL
95
+ # When alpha is disabled or all inputs are fully opaque, skip alpha
96
+ # interpolation and return #RRGGBB (not #RRGGBBAA).
97
+ _has_alpha = alpha and not np.all(alpha_array == 1.0)
98
+
99
+ def _ramp(x: ArrayLike) -> List[Optional[str]]:
100
+ x = np.asarray(x, dtype=float)
101
+ result: List[Optional[str]] = []
102
+ for val in x.flat:
103
+ if np.isnan(val) or val < 0.0 or val > 1.0:
104
+ # R: approxfun(rule=1) returns NA for out-of-[0,1],
105
+ # then encode_colour returns NA, mapped to na_color.
106
+ result.append(na_color)
107
+ else:
108
+ L = float(np.interp(val, x_in, lab_array[:, 0]))
109
+ a = float(np.interp(val, x_in, lab_array[:, 1]))
110
+ b = float(np.interp(val, x_in, lab_array[:, 2]))
111
+ r, g, bl = _lab_to_rgb(L, a, b)
112
+ if _has_alpha:
113
+ a_val = float(np.interp(val, x_in, alpha_array))
114
+ result.append(
115
+ to_hex((r, g, bl, a_val), keep_alpha=True)
116
+ )
117
+ else:
118
+ result.append(
119
+ to_hex((r, g, bl), keep_alpha=False)
120
+ )
121
+ return result
122
+
123
+ # R: new_continuous_palette(fun, type="colour", na_safe=!is.na(na.color))
124
+ return ContinuousPalette(
125
+ _ramp, type="colour", na_safe=na_color is not None
126
+ )