plot-io 0.12.0__py3-none-win_amd64.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.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-x86_64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/color/maps.py ADDED
@@ -0,0 +1,922 @@
1
+ """Color maps: every set of colors, with a kind.
2
+
3
+ A :class:`ColorMap` is a gradient over [0, 1]: its colors, their interpolation, the
4
+ colors below, above and of missing values, and its kind (``qualitative``,
5
+ ``sequential``, ``diverging``, ``cyclic`` or none), which decides how it colors
6
+ categories and numbers. Which values go to 0 and 1 (the limits, the scale) is the
7
+ business of a color axis (:mod:`plot_io.mapped`). One registry holds every map, the
8
+ qualitative ones included.
9
+
10
+ Colors are computed by plot_io's C library: smooth, spline and Oklab maps evaluated there,
11
+ bulk lookups through a table of 4,096 entries (steps exactly).
12
+ """
13
+
14
+ import array
15
+ import bisect
16
+ import functools
17
+ from collections.abc import Callable, Sequence
18
+ from typing import Any
19
+
20
+ from vec_io.paint import lchuv_to_srgb, oklab_to_srgb, srgb_to_oklab
21
+
22
+ from .. import kernels
23
+ from .._native import kernels as _native
24
+ from ..settings import one_of
25
+ from .data import KINDS, MAPS, QUALITATIVE, qualitative_colors
26
+ from ..units import Fixed
27
+ from ..validators import typed
28
+ from .values import VARIANTS, Named, color, parse_named, rgba_of, scale_color, split_colors
29
+
30
+ __all__ = ["ColorMap", "add_color_map", "color_map_names", "check_color_map", "check_map",
31
+ "check_series_map", "kind_settings", "COLOR_MAP_HINT", "as_color_map", "map_of", "chosen_map", "cycle_of", "hue", "KINDS_OF_MAPS",
32
+ "MAP_WORDS"]
33
+
34
+ RGBA = tuple[float, float, float, float]
35
+ #: Entries of the table that bulk lookups of continuous maps read.
36
+ LUT_SIZE = 4096
37
+ _CLEAR: RGBA = (0.0, 0.0, 0.0, 0.0)
38
+ #: Alphas closer to 1 than one 8-bit level are 1 (the gradients' rounding gives 1 - 2e-16).
39
+ _OPAQUE = 1.0 - 1.0 / 255
40
+
41
+
42
+ def _snapped(flat: array.array) -> array.array:
43
+ """``flat`` (RGBA rows) with alphas within one 8-bit level of 1 set to 1."""
44
+ for i in range(3, len(flat), 4):
45
+ if _OPAQUE < flat[i] < 1.0:
46
+ flat[i] = 1.0
47
+ return flat
48
+ _check_interpolation = one_of("linear", "nearest", "smooth", "spline")
49
+ #: The kinds of color maps.
50
+ KINDS_OF_MAPS = ("qualitative", "sequential", "diverging", "cyclic")
51
+ _check_kind = one_of(*KINDS_OF_MAPS)
52
+ #: Words of ``color_map`` choosing the map of a kind by the use: ``"auto"`` and the kinds.
53
+ MAP_WORDS = ("auto",) + KINDS_OF_MAPS
54
+ #: Colors of a qualitative function of n taken for its steps (numbers mapped onto it).
55
+ FUNCTION_STEPS = 10
56
+
57
+
58
+ def hue(n: int, c: float = 100.0, lum: float = 65.0, start: float = 15.0) -> tuple[str, ...]:
59
+ """ggplot2's default discrete colors (``scales::hue_pal()``): ``n`` hues evenly spaced
60
+ from ``start`` degrees, at chroma ``c`` and luminance ``lum``."""
61
+ n = max(n, 1)
62
+ return tuple("#" + "".join("%02X" % round(255 * v)
63
+ for v in lchuv_to_srgb(lum, c, start + 360.0 * i / n))
64
+ for i in range(n))
65
+
66
+
67
+ def _hex(c: Any) -> str:
68
+ """``"#RRGGBB"`` of a color (``"#RRGGBBAA"`` when translucent)."""
69
+ rgba = rgba_of(c)
70
+ if rgba is None:
71
+ raise ValueError(f"invalid color {c!r}")
72
+ out = "#" + "".join("%02X" % round(255 * v) for v in rgba[:3])
73
+ return out if len(rgba) < 4 or rgba[3] >= 1 else out + "%02X" % round(255 * rgba[3])
74
+
75
+
76
+ def _mix_lab(rgba: Sequence[float], lab: tuple[float, float, float], k: float
77
+ ) -> tuple[float, float, float, float]:
78
+ """``rgba`` moved by ``k`` toward ``lab`` in Oklab (alpha kept)."""
79
+ L, a, b = srgb_to_oklab(*rgba[:3])
80
+ r, g, bl = oklab_to_srgb(L + (lab[0] - L) * k, a + (lab[1] - a) * k, b + (lab[2] - b) * k)
81
+ return (r, g, bl, float(rgba[3]) if len(rgba) > 3 else 1.0)
82
+
83
+
84
+ def _mix(c: Any, lab: tuple[float, float, float], k: float) -> str:
85
+ """``c`` moved by ``k`` toward ``lab`` in Oklab, as hex."""
86
+ rgba = rgba_of(c)
87
+ if rgba is None:
88
+ raise ValueError(f"invalid color {c!r}")
89
+ return _hex(_mix_lab(rgba, lab, k))
90
+
91
+
92
+ def _fraction(k: Any) -> float:
93
+ from ..validators import real
94
+ x = real(k)
95
+ if x is None or not 0 <= x <= 1:
96
+ raise ValueError(f"expected an amount in 0–1, got {k!r}")
97
+ return x
98
+
99
+
100
+ def _in(c: Any, variant: str) -> Any:
101
+ """A checked color in ``variant`` (a named color without its own variant word)."""
102
+ return c.in_variant(variant) if isinstance(c, Named) else c.value if isinstance(c, Fixed) \
103
+ else c
104
+
105
+
106
+ def _sized(f: Callable[[int], Any], variant: str) -> Callable[[int], tuple[Any, ...]]:
107
+ """The colors of a function of n, checked, in ``variant``."""
108
+ def colors(n: int) -> tuple[Any, ...]:
109
+ try:
110
+ out = tuple(_in(color(c), variant) for c in f(max(n, 1)))
111
+ except (TypeError, ValueError) as e:
112
+ raise ValueError(f"color map function {f!r}: {e}") from None
113
+ if not out:
114
+ raise ValueError(f"color map function {f!r}: no colors")
115
+ return out
116
+ return colors
117
+
118
+
119
+ def _widen(colors: list[Any], count: int, sides: float) -> list[Any]:
120
+ """Each inner color ``count`` times, each end color ``round(sides × count)`` times."""
121
+ if isinstance(count, bool) or not isinstance(count, int) or count < 1:
122
+ raise ValueError(f"widen_colors must be a whole number ≥ 1, got {count!r}")
123
+ if not 0 <= sides <= 1:
124
+ raise ValueError(f"widen_sides must be in 0–1, got {sides!r}")
125
+ if count == 1 and sides == 1:
126
+ return colors
127
+ ends = round(sides * count)
128
+ if ends == 0 and len(colors) == 2:
129
+ raise ValueError(f"widen_sides={sides!r} with widen_colors={count!r} leaves no end "
130
+ "colors, and a list of two colors has no others")
131
+ if len(colors) == 1:
132
+ return [colors[0]] * max(ends, 1)
133
+ return [colors[0]] * ends + [c for c in colors[1:-1] for _ in range(count)] \
134
+ + [colors[-1]] * ends
135
+
136
+
137
+ def _rgba(color: Any, variant: str = "base") -> RGBA:
138
+ if isinstance(color, Named):
139
+ color = color.in_variant(variant)
140
+ elif isinstance(color, Fixed):
141
+ color = color.value
142
+ try:
143
+ c = rgba_of(scale_color(color, variant) or color)
144
+ except (TypeError, ValueError):
145
+ c = None
146
+ if c is None:
147
+ raise ValueError(f"invalid color {color!r}")
148
+ return (float(c[0]), float(c[1]), float(c[2]), float(c[3]) if len(c) > 3 else 1.0)
149
+
150
+
151
+ def _named(c: Any) -> bool:
152
+ """Whether ``c`` is a named color without a variant word."""
153
+ if isinstance(c, Named):
154
+ return c.variant is None
155
+ try:
156
+ p = parse_named(c)
157
+ except ValueError:
158
+ return False
159
+ return p is not None and p[2] is None
160
+
161
+
162
+ def _hex_stops(data: str) -> list[tuple[float, RGBA]]:
163
+ colors = [(int(data[i:i + 2], 16) / 255, int(data[i + 2:i + 4], 16) / 255,
164
+ int(data[i + 4:i + 6], 16) / 255, 1.0) for i in range(0, len(data), 6)]
165
+ n = len(colors) - 1
166
+ return [(i / n, c) for i, c in enumerate(colors)]
167
+
168
+
169
+ class _Default:
170
+ """The value of an extreme not given (the map's own, or none); ``None`` is none."""
171
+
172
+ def __repr__(self) -> str:
173
+ return "DEFAULT"
174
+
175
+
176
+ DEFAULT: Any = _Default()
177
+
178
+ Stops = tuple[tuple[float, RGBA], ...]
179
+ #: Registered maps, whole (extremes included), by name (filled below the class).
180
+ _REGISTRY: dict[str, "ColorMap"] = {}
181
+ _ALIASES = {"gray": "grey"}
182
+ #: Short prefixes of map names: ``"bwr"`` is ``"blue_white_red"``, ``"bwr_hc"``
183
+ #: ``"blue_white_red_hc"``.
184
+ _PREFIXES = {"bwr": "blue_white_red"}
185
+
186
+
187
+ def _expand(name: str) -> str:
188
+ """``name`` with a short prefix written out (``"bwr_hc_r"`` → ``"blue_white_red_hc_r"``)."""
189
+ head, sep, tail = name.partition("_")
190
+ full = _PREFIXES.get(head)
191
+ return name if full is None else full + sep + tail
192
+
193
+
194
+ def color_map_names(kind: str | None = None) -> list[str]:
195
+ """The registered names, sorted: all, or those of maps of ``kind``."""
196
+ if kind is None:
197
+ return sorted(_REGISTRY)
198
+ kind = _check_kind(kind)
199
+ return sorted(n for n, cm in _REGISTRY.items() if cm.kind == kind)
200
+
201
+
202
+ def add_color_map(name: str, color_map: Any, replace: bool = False,
203
+ kind: str | None = None) -> None:
204
+ """Register a map, extremes included, under ``name`` (also usable reversed, as
205
+ ``name + "_r"``, and with a variant word first): a ``ColorMap`` or anything its
206
+ ``colors`` accepts, of ``kind`` when given (``kind="qualitative"``: a list of colors
207
+ taken in turn, or a function ``n -> colors``). A bundled map is replaced only with
208
+ ``replace=True``."""
209
+ key = name.strip().lower() if isinstance(name, str) else ""
210
+ if not key:
211
+ raise ValueError(f"a color map name is a non-empty string, got {name!r}")
212
+ if key.endswith("_r"):
213
+ raise ValueError(f"color map name {name!r}: a name ending in _r is the reversed map "
214
+ f"of the name before it")
215
+ if key in _ALIASES:
216
+ raise ValueError(f"color map name {name!r} is another name of {_ALIASES[key]!r}")
217
+ if _expand(key) != key:
218
+ raise ValueError(f"color map name {name!r} is short for {_expand(key)!r}")
219
+ if key.partition(" ")[2] and key.partition(" ")[0] in VARIANTS:
220
+ raise ValueError(f"color map name {name!r}: a variant word first gives a map in "
221
+ f"that variant")
222
+ if key in _BUNDLED and not replace:
223
+ raise ValueError(f"{name!r} is a bundled color map: give replace=True to replace it")
224
+ if isinstance(color_map, ColorMap) and (kind is None or kind == color_map.kind):
225
+ _REGISTRY[key] = color_map
226
+ else:
227
+ _REGISTRY[key] = ColorMap(color_map, kind=kind)
228
+
229
+
230
+ def _reverse_stops(stops: Stops, interpolation: str) -> list[tuple[float, RGBA]]:
231
+ """Stops of the reversed map (steps start at their stop: their edges flip)."""
232
+ if interpolation == "nearest":
233
+ ends = [p for p, _ in stops[1:]] + [1.0]
234
+ return [(1.0 - e, c) for e, (_, c) in zip(reversed(ends), reversed(stops))]
235
+ return [(1.0 - p, c) for p, c in reversed(stops)]
236
+
237
+
238
+ def _split(key: str) -> tuple[str | None, str]:
239
+ """``(variant word or None, name)`` of ``"pastel tab10_custom"``."""
240
+ head, _, rest = key.partition(" ")
241
+ if rest and head in VARIANTS:
242
+ return head, rest.strip()
243
+ return None, key
244
+
245
+
246
+ def _lookup(name: str) -> tuple["ColorMap", str]:
247
+ """The registered map ``name`` (reversed for ``_r``; in a variant for a variant word
248
+ first: ``"pastel tab10_custom"``), and its key."""
249
+ key = " ".join(name.strip().lower().split())
250
+ variant, rest = _split(key)
251
+ rest = _expand(rest)
252
+ reverse = rest.endswith("_r") and rest not in _REGISTRY
253
+ base = rest[:-2] if reverse else rest
254
+ base = _ALIASES.get(base, base)
255
+ if base not in _REGISTRY:
256
+ import difflib
257
+ near = difflib.get_close_matches(rest, _REGISTRY, n=1)
258
+ hint = f" (did you mean {near[0]!r}?)" if near else ""
259
+ raise ValueError(f"unknown color map {name!r}{hint}")
260
+ cm = _REGISTRY[base]
261
+ if variant is not None:
262
+ if cm._recolor is None:
263
+ raise ValueError(f"color map {name!r}: {base!r} has no named colors for the "
264
+ f"variant {variant!r} to recolor")
265
+ cm = cm.in_variant(variant)
266
+ return (cm.reversed() if reverse else cm), key
267
+
268
+
269
+ class ColorMap:
270
+ """A set of colors of a kind: a gradient over [0, 1].
271
+
272
+ ``colors``: a registered name (``"viridis"``, ``"viridis_r"`` reversed,
273
+ ``"pastel tab10_custom"`` a map of named colors in one variant), a list of colors
274
+ (evenly spaced, or at ``positions``), another ``ColorMap`` (copied) or a function
275
+ ``t -> color`` (with ``kind="qualitative"``: a function ``n -> colors``, called with
276
+ the number of colors needed); default ``"rainbow_lc"`` (not a
277
+ style's ``sequential_map``).
278
+
279
+ ``kind``: ``"qualitative"``, ``"sequential"``, ``"diverging"``, ``"cyclic"`` or
280
+ ``None`` (default; a copied or registered map keeps its own). Categories and series
281
+ take the colors of a qualitative map, or of a map of no kind, in turn (cycling), and
282
+ ``n`` colors evenly spaced from the others (end to end; a cyclic map's ``n`` around
283
+ its circle): :meth:`colors`. Numbers on a qualitative map are steps, one color per
284
+ bin (its ``interpolation`` defaults to ``"nearest"``); on the others they are
285
+ interpolated. ``under``/``over``: colors below 0 and above 1, the values past the
286
+ color axis' limits (default: the end colors); ``bad``: color of missing values
287
+ (default: transparent). Not given, a copied or registered map keeps its own; ``None``
288
+ is none. ``positions`` apply to a list of colors only.
289
+ ``space``: interpolation between the colors, ``"oklab"`` (default: perceptually even)
290
+ or ``"srgb"``. ``interpolation``: ``"linear"`` (default) between neighboring colors;
291
+ ``"nearest"``, equal steps (``n`` colors give ``n`` bins; with ``positions``, each step
292
+ starts at its position); ``"smooth"``, a Bézier curve with the colors as control
293
+ points (through the first and last only; no ``positions``); ``"spline"``, a monotone
294
+ cubic curve through every color (no overshoot between colors).
295
+
296
+ ``widen_colors`` repeats each inner color of a list that many times, and each end
297
+ color ``round(widen_sides × widen_colors)`` times, widening the band of each color.
298
+ A list can be one string, the colors separated by commas.
299
+
300
+ A map is a value: immutable (the methods return changed copies), equal to another with
301
+ the same stops, space, interpolation, extremes and kind (the name aside), and hashable.
302
+
303
+ ``cm(t)``: the color at t; ``colors_at(ts)``: bulk colors (continuous maps through a
304
+ table of 4,096 colors, steps exactly); alphas within 1/255 of 1 are drawn as 1;
305
+ ``table(n)``: n colors sampled evenly; ``colors(n)``: the colors categories take
306
+ (above); ``cells(values, lo, hi, mode)``:
307
+ what heatmap cells hand vec_io's ``draw_cells``. ``reversed()`` (under and over
308
+ swapped), ``resampled(n)`` (n flat steps), ``truncated(lo, hi)`` (a part stretched to
309
+ [0, 1]), ``with_extremes(…)``, ``lighter(k=0.25)``, ``darker(k=0.25)`` and
310
+ ``mix(color, k)`` (each color moved by ``k`` toward white, black or ``color`` in
311
+ Oklab) and ``in_variant(variant)`` return copies. ``repr`` of a registered map is ``ColorMap('viridis')``.
312
+ A map is a set of colors only: the limits and scale of the values are their color
313
+ axis'.
314
+
315
+ Bundled (names in any case; ``_r`` reverses): viridis, magma, inferno, plasma (van der
316
+ Walt and Smith, CC0); cividis (Nuñez, Anderton and Renslow); coolwarm (Moreland);
317
+ rocket, mako, flare, crest, vlag, icefire (seaborn, Waskom, BSD-3-Clause); jet; a
318
+ parula-like map; turbo (Mikhailov, Google, Apache-2.0); ``ggplot2`` (``#132B43`` →
319
+ ``#56B1F7``); the rainbows from named colors (violet, sky, green, yellow, orange, rose;
320
+ ``widen_colors=6, widen_sides=0.6``, ``"smooth"``): ``rainbow`` (level 0.5),
321
+ ``rainbow_lc`` (0.4, the default ``sequential_map``), ``rainbow_l0`` (from white, then 0.083 to 0.5),
322
+ ``rainbow_l0hh`` (from white, then 0.1 to 0.6),
323
+ ``rainbow_llhh`` (0.16 to 0.6), ``rainbow_llh`` (0.16 to
324
+ 0.4), ``rainbow_hlh`` (dark ends, light middle); diverging ``blue_white_red``,
325
+ ``blue_white_red_hc`` (the default ``diverging_map``), ``blue_white_red_smooth`` (smooth
326
+ interpolation), ``blue_white_red_smooth_hc`` (``bwr`` is short
327
+ for ``blue_white_red``: ``"bwr"``, ``"bwr_hc"``, ``"bwr_smooth_r"``), ``blue_gray_red``, ``blue_gray_red_hc``; ColorBrewer's (Brewer,
328
+ Apache-2.0) sequential ``blues`` … ``ylorrd`` (9 colors) and diverging ``brbg``, ``piyg``,
329
+ ``prgn``, ``puor``, ``rdbu``, ``rdgy``, ``rdylbu``, ``rdylgn``, ``spectral`` (11);
330
+ Crameri's Scientific colour maps 8 (MIT): sequential ``batlow`` … ``navia``, diverging
331
+ ``broc``, ``cork``, ``vik``, ``lisbon``, ``tofino``, ``berlin``, ``roma``, ``bam``,
332
+ ``vanimo``, ``managua``, multi-sequential ``oleron``, ``bukavu``, ``fes`` (diverging,
333
+ their break at the centre), cyclic ``romao``, ``bamo``, ``broco``, ``corko``,
334
+ ``viko``. Qualitative: ``tab10_custom`` (the default; tab10's hues as named colors: sky,
335
+ amber, emerald, rose, purple 0.4, brown, pink 0.4, gray, lime, cyan);
336
+ ``tableau10_custom`` (Tableau 10's hues as named colors: sky, amber, rose, teal 0.4,
337
+ green, yellow, fuchsia, pink 0.4, brown, gray 0.4); ``qual`` (gray, yellow, emerald, rose, sky, brown, purple, lime, cyan); ``okabe_ito``
338
+ (Okabe–Ito's eight colorblind-safe colors plus indigo and dark brown); ``tab10``
339
+ (matplotlib ≥ 2.0, d3's category10); ``matplotlib_classic``; ``tableau10`` (Vega-Lite);
340
+ seaborn's ``deep``, ``muted``, ``pastel``, ``bright``, ``dark``, ``colorblind``;
341
+ ``hue`` (ggplot2's ``hue_pal()``: n hues 360/n apart from 15°); ``matlab``;
342
+ ``chartjs``; ``plotly``; ``observable10``; ``office``; ``economist``;
343
+ ``fivethirtyeight``; ColorBrewer's ``set1``, ``set2``, ``set3``, ``paired``,
344
+ ``accent``, ``dark2``, ``pastel1``, ``pastel2``; Crameri's categorical sets
345
+ ``batlow_s`` … ``navia_s`` (100 colors, the first differing most). The maps not named
346
+ diverging, cyclic or qualitative here are sequential. ``plt.registry.color_maps.names(kind=…)``
347
+ lists them.
348
+ """
349
+
350
+ __slots__ = ("stops", "space", "interpolation", "under", "over", "bad", "name", "kind",
351
+ "_colors", "_function", "_positions", "_key", "_hash", "_lut", "_recolor",
352
+ "_variants")
353
+
354
+ stops: Stops
355
+ space: str
356
+ interpolation: str
357
+ under: RGBA | None
358
+ over: RGBA | None
359
+ bad: RGBA | None
360
+ name: str | None
361
+ kind: str | None
362
+ #: the colors taken in turn (a list, or a qualitative map's), in this map's variant
363
+ _colors: tuple[Any, ...] | None
364
+ #: a qualitative function ``n -> colors`` (checked, in this map's variant)
365
+ _function: Callable[[int], tuple[Any, ...]] | None
366
+ _positions: list[float]
367
+ _key: tuple[Any, ...]
368
+ _hash: int | None
369
+ _lut: array.array | None
370
+ #: the map in a color variant (``variant -> ColorMap``), when built from named colors
371
+ _recolor: Any
372
+ _variants: dict[str, "ColorMap"] | None
373
+
374
+ def __init__(self, colors: Any = "rainbow_lc", positions: Sequence[float] | None = None,
375
+ under: Any = DEFAULT, over: Any = DEFAULT, bad: Any = DEFAULT,
376
+ space: str | None = None, name: str | None = None,
377
+ interpolation: str | None = None, widen_colors: int = 1,
378
+ widen_sides: float = 1.0, kind: str | None = None,
379
+ _variant: str = "base") -> None:
380
+ if not (isinstance(colors, (ColorMap, str)) or callable(colors)):
381
+ colors = list(colors)
382
+ args = (colors, positions, under, over, bad, space, name, interpolation, widen_colors,
383
+ widen_sides, kind)
384
+ kind = None if kind is None else _check_kind(kind)
385
+ varies = False
386
+ interp = "linear"
387
+ mode = None if interpolation is None else _check_interpolation(interpolation)
388
+ if mode == "smooth" and positions is not None:
389
+ raise ValueError("smooth interpolation takes no positions (the colors are "
390
+ "Bézier control points)")
391
+ even = False # stops evenly spaced (re-spaced for steps)
392
+ default_space = "oklab"
393
+ stops: list[tuple[float, RGBA]]
394
+ listed: tuple[Any, ...] | None = None
395
+ function: Callable[[int], tuple[Any, ...]] | None = None
396
+ own_kind: str | None = None
397
+ even_steps = False
398
+ if isinstance(colors, str) and (parts := split_colors(colors)) is not None:
399
+ colors = parts # "violet 0.4, sky 0.4, …"
400
+ is_list = not (isinstance(colors, (ColorMap, str)) or callable(colors))
401
+ if (widen_colors != 1 or widen_sides != 1.0) and not is_list:
402
+ raise ValueError("widen_colors and widen_sides apply to lists of colors")
403
+ if positions is not None and not is_list:
404
+ raise ValueError("positions apply to a list of colors")
405
+ own: tuple[Any, Any, Any] = (None, None, None) # the extremes of a map copied
406
+ if isinstance(colors, str):
407
+ colors, key = _lookup(colors)
408
+ name = name or key
409
+ copied = isinstance(colors, ColorMap)
410
+ if copied:
411
+ varies = colors._recolor is not None
412
+ colors = colors.in_variant(_variant)
413
+ stops, default_space = list(colors.stops), colors.space
414
+ interp = colors.interpolation
415
+ n = max(len(stops) - 1, 1)
416
+ even = colors.interpolation != "nearest" and all(
417
+ abs(p - i / n) < 1e-12 for i, (p, _) in enumerate(stops))
418
+ even_steps = colors.interpolation == "nearest" and len(stops) > 1 and all(
419
+ abs(p - i / len(stops)) < 1e-12 for i, (p, _) in enumerate(stops))
420
+ name = name or colors.name
421
+ own = (colors.under, colors.over, colors.bad)
422
+ listed, function, own_kind = colors._colors, colors._function, colors.kind
423
+ elif callable(colors) and kind == "qualitative":
424
+ function = _sized(colors, _variant)
425
+ cols = [_rgba(c) for c in function(FUNCTION_STEPS)]
426
+ varies = any(_named(c) for c in colors(FUNCTION_STEPS))
427
+ stops = [(i / len(cols), c) for i, c in enumerate(cols)]
428
+ interp = "nearest"
429
+ elif callable(colors):
430
+ try:
431
+ stops = [(i / 255, _rgba(colors(i / 255))) for i in range(256)]
432
+ except TypeError as e:
433
+ raise ValueError(f"color map function {colors!r}: {e} (a function t -> "
434
+ f"color; one of n -> colors takes kind=\"qualitative\")"
435
+ ) from None
436
+ else:
437
+ varies = any(_named(c) for c in colors)
438
+ cols = _widen([_rgba(c, _variant) for c in colors], widen_colors, widen_sides)
439
+ if not cols:
440
+ raise ValueError("a color map needs at least one color")
441
+ if positions is not None and (widen_colors != 1 or widen_sides != 1.0):
442
+ raise ValueError("widen_colors and widen_sides take no positions")
443
+ if positions is None:
444
+ n = max(len(cols) - 1, 1)
445
+ stops = [(i / n, c) for i, c in enumerate(cols)]
446
+ even = True
447
+ else:
448
+ pos = [float(p) for p in positions]
449
+ if len(pos) != len(cols):
450
+ raise ValueError("give one position per color")
451
+ if any(b < a for a, b in zip(pos, pos[1:])) or pos[0] < 0 or pos[-1] > 1:
452
+ raise ValueError("positions must increase within [0, 1]")
453
+ stops = list(zip(pos, cols))
454
+ if kind in (None, "qualitative"):
455
+ try:
456
+ listed = tuple(_in(color(c), _variant) for c in colors)
457
+ except (TypeError, ValueError):
458
+ listed = tuple(_hex(c) for c in cols)
459
+ if kind is None:
460
+ kind = own_kind
461
+ elif copied and kind != own_kind:
462
+ listed = function = None # another kind: its colors come from its stops
463
+ if mode is None and (interp == "nearest") != (kind == "qualitative"):
464
+ mode = "nearest" if kind == "qualitative" else "linear"
465
+ if kind == "qualitative" and is_list and mode is None:
466
+ mode = "nearest"
467
+ if even_steps and mode not in (None, "nearest"):
468
+ n = len(stops) - 1 # even steps → evenly spaced colors
469
+ stops = [(i / n, c) for i, (_, c) in enumerate(stops)]
470
+ if mode is not None:
471
+ if mode == "nearest" and even:
472
+ stops = [(i / len(stops), c) for i, (_, c) in enumerate(stops)]
473
+ interp = mode
474
+ varies = varies or any(_named(v) for v in (under, over, bad))
475
+ under, over, bad = (o if v is DEFAULT else v for v, o in zip((under, over, bad), own))
476
+ self._init(stops, one_of("oklab", "srgb")(space) if space is not None else default_space,
477
+ interp, None if under is None else _rgba(under, _variant),
478
+ None if over is None else _rgba(over, _variant),
479
+ None if bad is None else _rgba(bad, _variant), name, kind, listed, function)
480
+ if varies and _variant == "base":
481
+ object.__setattr__(self, "_recolor", lambda v: ColorMap(*args, _variant=v))
482
+
483
+ def _init(self, stops: Sequence[tuple[float, RGBA]], space: str, interpolation: str,
484
+ under: RGBA | None, over: RGBA | None, bad: RGBA | None,
485
+ name: str | None, kind: str | None = None, colors: tuple[Any, ...] | None = None,
486
+ function: Callable[[int], tuple[Any, ...]] | None = None) -> None:
487
+ if len(stops) == 1:
488
+ stops = [(0.0, stops[0][1]), (1.0, stops[0][1])]
489
+ frozen = tuple((float(p), tuple(c)) for p, c in stops)
490
+ put = object.__setattr__
491
+ for k, v in (("stops", frozen), ("space", space), ("interpolation", interpolation),
492
+ ("under", under), ("over", over), ("bad", bad), ("name", name),
493
+ ("kind", kind), ("_colors", colors), ("_function", function),
494
+ ("_positions", [p for p, _ in frozen]), ("_hash", None), ("_lut", None),
495
+ ("_recolor", None), ("_variants", None)):
496
+ put(self, k, v)
497
+ put(self, "_key", (frozen, space, interpolation, under, over, bad, kind, function))
498
+
499
+ @classmethod
500
+ def _new(cls, stops: Sequence[tuple[float, RGBA]], space: str, interpolation: str,
501
+ under: RGBA | None, over: RGBA | None, bad: RGBA | None,
502
+ name: str | None, kind: str | None = None, colors: tuple[Any, ...] | None = None,
503
+ function: Callable[[int], tuple[Any, ...]] | None = None) -> "ColorMap":
504
+ """A map of these fields as they are (colors already RGBA)."""
505
+ cm = object.__new__(cls)
506
+ cm._init(stops, space, interpolation, under, over, bad, name, kind, colors, function)
507
+ return cm
508
+
509
+ def in_variant(self, variant: str) -> "ColorMap":
510
+ """This map with its named colors (those without a variant word) in ``variant``:
511
+ itself when it has none."""
512
+ if variant == "base" or self._recolor is None:
513
+ return self
514
+ cache = self._variants
515
+ if cache is None:
516
+ cache = {}
517
+ object.__setattr__(self, "_variants", cache)
518
+ out = cache.get(variant)
519
+ if out is None:
520
+ out = cache[variant] = self._recolor(variant)
521
+ return out
522
+
523
+ def _derived(self, out: "ColorMap", f: Any) -> "ColorMap":
524
+ """``out``, made by ``f`` from this map, following it into each variant."""
525
+ if self._recolor is not None:
526
+ object.__setattr__(out, "_recolor", lambda v: f(self.in_variant(v)))
527
+ return out
528
+
529
+ def __setattr__(self, key: str, value: Any) -> None:
530
+ raise AttributeError("a ColorMap is immutable: with_extremes(), reversed(), "
531
+ "truncated(), resampled() or ColorMap(color_map, …) make changed copies")
532
+
533
+ def __delattr__(self, key: str) -> None:
534
+ self.__setattr__(key, None)
535
+
536
+ def __eq__(self, other: object) -> bool:
537
+ if not isinstance(other, ColorMap):
538
+ return NotImplemented
539
+ return self is other or self._key == other._key
540
+
541
+ def __hash__(self) -> int:
542
+ h = self._hash
543
+ if h is None:
544
+ h = hash(self._key)
545
+ object.__setattr__(self, "_hash", h)
546
+ return h
547
+
548
+ def __copy__(self) -> "ColorMap":
549
+ return self
550
+
551
+ def __deepcopy__(self, memo: Any) -> "ColorMap":
552
+ return self
553
+
554
+ def __reduce__(self) -> Any:
555
+ return (ColorMap._new, (self.stops, self.space, self.interpolation, self.under,
556
+ self.over, self.bad, self.name, self.kind, self._colors,
557
+ self._function))
558
+
559
+ def __repr__(self) -> str:
560
+ """``ColorMap('name')`` for a registered map unchanged, else the arguments that make
561
+ an equal map."""
562
+ if self.name:
563
+ try:
564
+ if ColorMap(self.name) == self:
565
+ return f"ColorMap({self.name!r})"
566
+ except ValueError:
567
+ pass
568
+ stops = self.stops
569
+ n = len(stops)
570
+ args = [repr([c for _, c in stops])]
571
+ even = [i / n for i in range(n)] if self.interpolation == "nearest" \
572
+ else [i / max(n - 1, 1) for i in range(n)]
573
+ if self.interpolation != "smooth" and [p for p, _ in stops] != even:
574
+ args.append(f"positions={[p for p, _ in stops]!r}")
575
+ for key in ("under", "over", "bad"):
576
+ if getattr(self, key) is not None:
577
+ args.append(f"{key}={getattr(self, key)!r}")
578
+ if self.space != "oklab":
579
+ args.append(f"space={self.space!r}")
580
+ if self.interpolation != "linear":
581
+ args.append(f"interpolation={self.interpolation!r}")
582
+ if self.name:
583
+ args.append(f"name={self.name!r}")
584
+ if self.kind is not None:
585
+ args.append(f"kind={self.kind!r}")
586
+ return f"ColorMap({', '.join(args)})"
587
+
588
+ def _copy(self, **changes: Any) -> "ColorMap":
589
+ for key in ("under", "over", "bad"):
590
+ if changes.get(key) is not None:
591
+ changes[key] = _rgba(changes[key])
592
+ fields: dict[str, Any] = dict(
593
+ stops=self.stops, space=self.space, interpolation=self.interpolation,
594
+ under=self.under, over=self.over, bad=self.bad, name=self.name, kind=self.kind,
595
+ colors=self._colors, function=self._function)
596
+ fields.update(changes)
597
+ return ColorMap._new(**fields)
598
+
599
+ def __call__(self, t: float) -> RGBA:
600
+ """Color at ``t`` in [0, 1] (``under``/``over`` outside, ``bad`` for NaN)."""
601
+ t = float(t)
602
+ if t != t:
603
+ return self.bad if self.bad is not None else _CLEAR
604
+ if t < 0 and self.under is not None:
605
+ return self.under
606
+ if t > 1 and self.over is not None:
607
+ return self.over
608
+ return self._at(t)
609
+
610
+ def _at(self, t: float) -> RGBA:
611
+ """The gradient at ``t`` (clamped to the end colors)."""
612
+ stops, pos = self.stops, self._positions
613
+ if self.interpolation == "nearest": # step k covers [pos[k], pos[k + 1])
614
+ return stops[max(bisect.bisect_right(pos, t) - 1, 0)][1]
615
+ c = self._eval([t])
616
+ return (c[0], c[1], c[2], c[3])
617
+
618
+ def _eval(self, ts: Sequence[float]) -> array.array:
619
+ return _snapped(_native.colormap_eval(self.stops, self.interpolation, self.space,
620
+ array.array("d", ts)))
621
+
622
+ def table(self, n: int = 256) -> list[RGBA]:
623
+ """``n`` colors sampled evenly over [0, 1]."""
624
+ ts = [i / (n - 1) for i in range(n)] if n > 1 else [0.0]
625
+ if self.interpolation != "nearest":
626
+ flat = self._eval(ts)
627
+ return [(flat[4 * i], flat[4 * i + 1], flat[4 * i + 2], flat[4 * i + 3])
628
+ for i in range(len(ts))]
629
+ return [self._at(t) for t in ts]
630
+
631
+ def colors_at(self, ts: Any) -> Any:
632
+ """Colors at many positions in [0, 1] (an ``(n, 4)`` buffer, or a list): continuous
633
+ maps through a table of 4,096 entries, steps exactly."""
634
+ values = kernels.doubles(ts)
635
+ if self.interpolation == "nearest":
636
+ steps = array.array("d", self._positions)
637
+ table = _snapped(array.array("d", [v for _, c in self.stops for v in c]))
638
+ else:
639
+ lut = self._lut
640
+ if lut is None: # a cache of the value: set once
641
+ lut = array.array("d", [v for c in self.table(LUT_SIZE) for v in c])
642
+ object.__setattr__(self, "_lut", lut)
643
+ steps, table = None, lut
644
+ out = _native.colors_at(values, table, steps, self.under, self.over, self.bad)
645
+ n = len(out) // 4
646
+ return memoryview(out).cast("B").cast("d", (n, 4)) if n else []
647
+
648
+ def cells(self, values: Any, lo: float, hi: float, mode: str = "image"
649
+ ) -> tuple[array.array, list[RGBA], list[float] | None]:
650
+ """``(fill, colors, bounds)`` of vec_io's ``Canvas.draw_cells`` for ``values`` (an
651
+ ``array('d')``, through the color scale) over ``[lo, hi]``. Modes ``image`` and
652
+ ``cells``: indices into ``table(255)``, or into ``table(253)`` followed by the
653
+ under and over colors when the map has either. ``smooth``: positions along
654
+ ``table(256)``, or for steps along the step colors with ``bounds`` between them.
655
+ NaN values are bad cells; -inf and +inf are below and above the range."""
656
+ if mode == "smooth":
657
+ fill, _ = _native.cells(values, lo, hi, 0, False)
658
+ if self.interpolation != "nearest":
659
+ return fill, self.table(256), None
660
+ ends = self._positions[1:] + [1.0]
661
+ kept = [(p, c) for (p, c), e in zip(self.stops, ends) if e > p]
662
+ if not kept:
663
+ return fill, [self.stops[-1][1]], []
664
+ return fill, [c for _, c in kept], [p for p, _ in kept[1:]]
665
+ split = self.under is not None or self.over is not None
666
+ fill, _ = _native.cells(values, lo, hi, 253 if split else 255, split)
667
+ table = self.table(253 if split else 255)
668
+ if split:
669
+ table += [self.under or table[0], self.over or table[-1]]
670
+ return fill, table, None
671
+
672
+ def reversed(self) -> "ColorMap":
673
+ stops = _reverse_stops(self.stops, self.interpolation)
674
+ name = None
675
+ if self.name:
676
+ name = self.name[:-2] if self.name.endswith("_r") else self.name + "_r"
677
+ f = self._function
678
+ return self._derived(self._copy(
679
+ stops=stops, under=self.over, over=self.under, name=name,
680
+ colors=None if self._colors is None else self._colors[::-1],
681
+ function=None if f is None else lambda n: f(n)[::-1]), ColorMap.reversed)
682
+
683
+ def resampled(self, n: int) -> "ColorMap":
684
+ """``n`` flat steps (a discrete map)."""
685
+ if n < 1:
686
+ raise ValueError("resampled() needs at least one step")
687
+ cols = [self((i + 0.5) / n) for i in range(n)]
688
+ return self._derived(self._copy(stops=[(i / n, c) for i, c in enumerate(cols)],
689
+ name=None, interpolation="nearest", colors=None,
690
+ function=None),
691
+ lambda cm: cm.resampled(n))
692
+
693
+ def truncated(self, lo: float, hi: float) -> "ColorMap":
694
+ """The part between ``lo`` and ``hi``, stretched to [0, 1]."""
695
+ if not 0 <= lo < hi <= 1:
696
+ raise ValueError("truncated() needs 0 <= lo < hi <= 1")
697
+ if self.interpolation == "nearest": # steps stay steps
698
+ ends = self._positions[1:] + [1.0]
699
+ kept = [(max(p, lo), c) for (p, c), e in zip(self.stops, ends) if e > lo and p < hi]
700
+ out = self._copy(stops=[((p - lo) / (hi - lo), c) for p, c in kept], name=None,
701
+ interpolation="nearest", colors=None, function=None)
702
+ else:
703
+ n = 256
704
+ cols = [self(lo + (hi - lo) * i / (n - 1)) for i in range(n)]
705
+ out = self._copy(stops=[(i / (n - 1), c) for i, c in enumerate(cols)], name=None,
706
+ interpolation="linear", colors=None, function=None)
707
+ return self._derived(out, lambda cm: cm.truncated(lo, hi))
708
+
709
+ def colors(self, n: int | None = None) -> tuple[Any, ...]:
710
+ """The colors categories and series take: of a qualitative map or a map of no kind,
711
+ its colors in turn (``n`` of them, cycling; all of them when ``n`` is None; a
712
+ function of n is called with ``n``); of the other kinds, ``n`` colors evenly spaced
713
+ (end to end; a cyclic map's around its circle; the colors of its stops when ``n``
714
+ is None)."""
715
+ if n is not None and (isinstance(n, bool) or not isinstance(n, int) or n < 0):
716
+ raise ValueError(f"colors() takes a count ≥ 0, got {n!r}")
717
+ if self._function is not None:
718
+ return self._function(n or 1)
719
+ if self.kind in (None, "qualitative") or n is None:
720
+ cols = self._colors
721
+ if cols is None:
722
+ cols = tuple(_hex(c) for _, c in self.stops)
723
+ return cols if n is None else tuple(cols[i % len(cols)] for i in range(n))
724
+ if self.kind == "cyclic":
725
+ ts = [i / n for i in range(n)]
726
+ else:
727
+ ts = [i / (n - 1) for i in range(n)] if n > 1 else [0.5]
728
+ return tuple(_hex(self._at(t)) for t in ts)
729
+
730
+ def mix(self, color: Any, k: float = 0.5) -> "ColorMap":
731
+ """Each color (extremes included, missing values aside) moved by ``k`` (0–1)
732
+ toward ``color``, in Oklab."""
733
+ rgba = _rgba(color)
734
+ k = _fraction(k)
735
+ out = self._moved(srgb_to_oklab(*rgba[:3]), k)
736
+ if _named(color):
737
+ object.__setattr__(out, "_recolor", lambda v: self.in_variant(v).mix(
738
+ _rgba(color, v), k))
739
+ return out
740
+ return self._derived(out, lambda cm: cm.mix(color, k))
741
+
742
+ def lighter(self, k: float = 0.25) -> "ColorMap":
743
+ """Each color moved by ``k`` (0–1) toward white, in Oklab."""
744
+ k = _fraction(k)
745
+ return self._derived(self._moved((1.0, 0.0, 0.0), k), lambda cm: cm.lighter(k))
746
+
747
+ def darker(self, k: float = 0.25) -> "ColorMap":
748
+ """Each color moved by ``k`` (0–1) toward black, in Oklab."""
749
+ k = _fraction(k)
750
+ return self._derived(self._moved((0.0, 0.0, 0.0), k), lambda cm: cm.darker(k))
751
+
752
+ def _moved(self, lab: tuple[float, float, float], k: float) -> "ColorMap":
753
+ """This map with each color moved by ``k`` toward ``lab``."""
754
+ f = self._function
755
+ return self._copy(
756
+ stops=[(p, _mix_lab(c, lab, k)) for p, c in self.stops], name=None,
757
+ under=None if self.under is None else _mix_lab(self.under, lab, k),
758
+ over=None if self.over is None else _mix_lab(self.over, lab, k),
759
+ colors=None if self._colors is None else tuple(_mix(c, lab, k)
760
+ for c in self._colors),
761
+ function=None if f is None else lambda n: tuple(_mix(c, lab, k) for c in f(n)))
762
+
763
+ def with_extremes(self, under: Any = DEFAULT, over: Any = DEFAULT,
764
+ bad: Any = DEFAULT) -> "ColorMap":
765
+ """A copy with the extremes given changed (``None``: none)."""
766
+ given = {k: v for k, v in (("under", under), ("over", over), ("bad", bad))
767
+ if v is not DEFAULT}
768
+ out = self._copy(**given)
769
+ if any(_named(v) for v in given.values()):
770
+ object.__setattr__(out, "_recolor", lambda v: ColorMap(self.in_variant(
771
+ v), under=under, over=over, bad=bad, _variant=v))
772
+ return out
773
+ return self._derived(out, lambda cm: cm.with_extremes(under, over, bad))
774
+
775
+
776
+ def _with_colors(cm: ColorMap, colors: tuple[str, ...]) -> ColorMap:
777
+ """``cm`` with the colors it takes in turn written as ``colors``."""
778
+ object.__setattr__(cm, "_colors", colors)
779
+ return cm
780
+
781
+
782
+ for _name, _spec in MAPS.items(): # hex strings, or ColorMap arguments
783
+ _REGISTRY[_name] = ColorMap(**_spec, kind=KINDS[_name]) if isinstance(_spec, dict) else \
784
+ ColorMap._new(_hex_stops("".join(_spec)), "oklab", "linear", None, None, None, _name,
785
+ KINDS[_name])
786
+ for _name, _spec in QUALITATIVE.items(): # hex strings, or lists of named colors
787
+ _cm = _REGISTRY[_name] = ColorMap(
788
+ _spec if isinstance(_spec, list) else qualitative_colors(_name), name=_name,
789
+ kind="qualitative")
790
+ object.__setattr__(_cm, "_colors", qualitative_colors(_name)) # as "#RRGGBB"
791
+ if isinstance(_spec, list): # in every variant as "#RRGGBB" too
792
+ object.__setattr__(_cm, "_recolor", lambda v, cm=_cm._recolor, n=_name: _with_colors(
793
+ cm(v), qualitative_colors(n, v)))
794
+ _REGISTRY["hue"] = ColorMap(hue, name="hue", kind="qualitative")
795
+ _BUNDLED = frozenset(_REGISTRY)
796
+
797
+
798
+ def kind_settings() -> dict[str, Any]:
799
+ """The inherited settings of the map of each kind."""
800
+ from ..settings import Setting
801
+ return dict(
802
+ qualitative_map=Setting(check_map, inherited=True,
803
+ doc="Colors of series and categories, taken in turn (a map "
804
+ "of another kind: evenly spaced): a map name, a "
805
+ "`ColorMap`, colors or a function `n -> colors`."),
806
+ sequential_map=Setting(check_map, inherited=True,
807
+ doc="Map of numbers given as colors (`color_map=\"auto\"`)."),
808
+ diverging_map=Setting(check_map, inherited=True,
809
+ doc="Map of numbers about a `center` (`color_map=\"auto\"`)."),
810
+ cyclic_map=Setting(check_map, inherited=True,
811
+ doc="Map of `color_map=\"cyclic\"`."))
812
+
813
+
814
+ #: The error's hint for ``color_map`` on a figure, a plot or in a style.
815
+ COLOR_MAP_HINT = ("use qualitative_map=, sequential_map=, diverging_map= or cyclic_map= "
816
+ "(color_map is set on what is colored: a series, a color axis)")
817
+
818
+
819
+ def _word(v: Any) -> str | None:
820
+ """``v`` as a word of :data:`MAP_WORDS`, or ``None``."""
821
+ if isinstance(v, str):
822
+ w = v.strip().lower()
823
+ if w in MAP_WORDS:
824
+ return w
825
+ return None
826
+
827
+
828
+ def check_color_map(v: Any) -> Any:
829
+ """A ``color_map``: ``"auto"``, a kind (the map of that kind's setting), or a
830
+ ``ColorMap`` or anything its ``colors`` accepts (as a ``ColorMap``)."""
831
+ w = _word(v)
832
+ return w if w is not None else as_color_map(v)
833
+
834
+
835
+ @typed("ColorMap | str | Sequence[ColorLike] | ArrayLike | Callable[..., Any] | "
836
+ "Mapping[Any, ColorLike]")
837
+ def check_series_map(v: Any) -> Any:
838
+ """A series' ``color_map``: as :func:`check_color_map`, or a dict level → color."""
839
+ if isinstance(v, dict):
840
+ from .values import color
841
+ return {k: color(c) for k, c in v.items()}
842
+ return check_color_map(v)
843
+
844
+
845
+ def _one_argument(f: Callable[..., Any]) -> None:
846
+ """Refuse a function that cannot be called with one argument (``n`` or ``t``)."""
847
+ import inspect
848
+ try:
849
+ inspect.signature(f).bind(0)
850
+ except TypeError:
851
+ raise ValueError(f"color map function {f!r}: it must take one argument (n -> colors, "
852
+ f"or t -> color)") from None
853
+ except ValueError: # no signature (a builtin): try at use
854
+ pass
855
+
856
+
857
+ @typed("ColorMap | str | Sequence[ColorLike] | ArrayLike | Callable[..., Any]")
858
+ def check_map(v: Any) -> Any:
859
+ """A ``qualitative_map``, ``sequential_map``, ``diverging_map`` or ``cyclic_map``: a map
860
+ name (kept, lowercase), a ``ColorMap``, colors, or a function (``n -> colors`` for
861
+ ``qualitative_map``, else ``t -> color``; called when the colors are needed)."""
862
+ if isinstance(v, ColorMap):
863
+ return v
864
+ if callable(v) and not isinstance(v, str):
865
+ _one_argument(v)
866
+ return v
867
+ if _word(v) is not None:
868
+ raise ValueError(f"{v!r} is a value of color_map only (on what is colored); give "
869
+ f"a map name, a ColorMap or colors")
870
+ if isinstance(v, str) and split_colors(v) is None:
871
+ ColorMap(v)
872
+ return " ".join(v.strip().lower().split())
873
+ return as_color_map(v)
874
+
875
+
876
+ def map_of(obj: Any, kind: str, variant: str | None = None) -> ColorMap:
877
+ """The map of ``obj``'s ``<kind>_map`` setting, in ``variant`` (default: ``obj``'s color
878
+ variant)."""
879
+ from .values import variant_of
880
+ holder = obj
881
+ while f"{kind}_map" not in holder.SETTINGS: # a series: its plot's
882
+ holder = holder._parent()
883
+ v = holder.resolve(f"{kind}_map")
884
+ if callable(v) and not isinstance(v, ColorMap):
885
+ cm = ColorMap(v, kind="qualitative") if kind == "qualitative" else as_color_map(v)
886
+ else:
887
+ cm = as_color_map(v)
888
+ return cm.in_variant(variant_of(obj) if variant is None else variant)
889
+
890
+
891
+ def cycle_of(cm: ColorMap, n: int = 1) -> tuple[Any, ...]:
892
+ """The colors ``n`` series take in turn from ``cm``: every color of a list (cycled), or
893
+ ``n`` of a function or of a map of another kind (evenly spaced)."""
894
+ if cm._function is None and cm.kind in (None, "qualitative"):
895
+ return cm.colors()
896
+ return cm.colors(max(n, 1))
897
+
898
+
899
+ def chosen_map(obj: Any, value: Any, kind: str) -> ColorMap:
900
+ """The map a ``color_map`` value chooses, in ``obj``'s color variant: a kind word gives
901
+ ``obj``'s map of that kind, ``"auto"`` the map of ``kind`` (the use: ``"qualitative"``
902
+ for series and categories, ``"sequential"`` for numbers, ``"diverging"`` for numbers
903
+ about a centre)."""
904
+ from .values import variant_of
905
+ w = _word(value)
906
+ if w is not None:
907
+ return map_of(obj, kind if w == "auto" else w)
908
+ return as_color_map(value).in_variant(variant_of(obj))
909
+
910
+
911
+ @functools.lru_cache(maxsize=64)
912
+ def _shared(cm: ColorMap, varies: bool) -> ColorMap:
913
+ """The first of the equal maps made recently (one table of colors per value)."""
914
+ return cm
915
+
916
+
917
+ def as_color_map(value: Any) -> ColorMap:
918
+ """``value`` as a ``ColorMap`` (a map given is returned as it is)."""
919
+ if isinstance(value, ColorMap):
920
+ return value
921
+ cm = ColorMap(value)
922
+ return _shared(cm, cm._recolor is not None)