plotpress 0.23.2__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.
- plotpress/__init__.py +108 -0
- plotpress/_interactive.py +2849 -0
- plotpress/_spectral.py +154 -0
- plotpress/_version.py +1 -0
- plotpress/artists.py +1382 -0
- plotpress/axes.py +3221 -0
- plotpress/colors.py +498 -0
- plotpress/figure.py +3084 -0
- plotpress/fonts/__init__.py +51 -0
- plotpress/fonts/families.py +192 -0
- plotpress/fonts/installed.py +82 -0
- plotpress/fonts/metrics.py +265 -0
- plotpress/png.py +93 -0
- plotpress/polar.py +240 -0
- plotpress/primitives.py +335 -0
- plotpress/qt.py +427 -0
- plotpress/raster.py +1316 -0
- plotpress/style.py +91 -0
- plotpress/svg.py +2589 -0
- plotpress/ticker.py +212 -0
- plotpress/transform.py +85 -0
- plotpress/vega.py +1324 -0
- plotpress/vega_lite.py +1199 -0
- plotpress-0.23.2.dist-info/METADATA +378 -0
- plotpress-0.23.2.dist-info/RECORD +28 -0
- plotpress-0.23.2.dist-info/WHEEL +5 -0
- plotpress-0.23.2.dist-info/licenses/LICENSE +21 -0
- plotpress-0.23.2.dist-info/top_level.txt +1 -0
plotpress/colors.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Colormaps and normalization for ``pcolormesh`` / mapped scatter.
|
|
2
|
+
|
|
3
|
+
Colormaps are lookup tables (256x3 uint8). ``viridis`` is stored as a small set
|
|
4
|
+
of anchor stops and linearly interpolated to 256 entries at import time to keep
|
|
5
|
+
the source compact while staying visually faithful.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
# Viridis anchor stops at t = 0.0, 0.1, ... 1.0 (RGB 0-255).
|
|
16
|
+
_VIRIDIS_ANCHORS = np.array([
|
|
17
|
+
[68, 1, 84], [72, 40, 120], [62, 74, 137], [49, 104, 142],
|
|
18
|
+
[38, 130, 142], [31, 158, 137], [53, 183, 121], [110, 206, 88],
|
|
19
|
+
[181, 222, 43], [221, 227, 24], [253, 231, 37],
|
|
20
|
+
], dtype=float)
|
|
21
|
+
|
|
22
|
+
# Plasma anchor stops.
|
|
23
|
+
_PLASMA_ANCHORS = np.array([
|
|
24
|
+
[13, 8, 135], [84, 2, 163], [139, 10, 165], [185, 50, 137],
|
|
25
|
+
[219, 92, 104], [244, 136, 73], [254, 188, 43], [240, 249, 33],
|
|
26
|
+
], dtype=float)
|
|
27
|
+
|
|
28
|
+
# Inferno / magma / cividis: the rest of the perceptually-uniform family.
|
|
29
|
+
_INFERNO_ANCHORS = np.array([
|
|
30
|
+
[0, 0, 4], [40, 11, 84], [101, 21, 110], [159, 42, 99],
|
|
31
|
+
[212, 72, 66], [245, 125, 21], [250, 193, 39], [252, 255, 164],
|
|
32
|
+
], dtype=float)
|
|
33
|
+
|
|
34
|
+
_MAGMA_ANCHORS = np.array([
|
|
35
|
+
[0, 0, 4], [28, 16, 68], [79, 18, 123], [129, 37, 129],
|
|
36
|
+
[181, 54, 122], [229, 80, 100], [251, 135, 97], [254, 194, 135],
|
|
37
|
+
[252, 253, 191],
|
|
38
|
+
], dtype=float)
|
|
39
|
+
|
|
40
|
+
_CIVIDIS_ANCHORS = np.array([
|
|
41
|
+
[0, 32, 76], [0, 42, 102], [45, 63, 108], [87, 86, 109],
|
|
42
|
+
[124, 109, 107], [165, 133, 93], [210, 160, 68], [255, 234, 70],
|
|
43
|
+
], dtype=float)
|
|
44
|
+
|
|
45
|
+
# Coolwarm: a blue-white-red diverging map (for signed data around a midpoint).
|
|
46
|
+
_COOLWARM_ANCHORS = np.array([
|
|
47
|
+
[59, 76, 192], [124, 159, 249], [192, 212, 245], [221, 221, 221],
|
|
48
|
+
[246, 193, 169], [241, 133, 103], [180, 4, 38],
|
|
49
|
+
], dtype=float)
|
|
50
|
+
|
|
51
|
+
# RdBu: red-white-blue diverging (coolwarm's classic ColorBrewer cousin).
|
|
52
|
+
_RDBU_ANCHORS = np.array([
|
|
53
|
+
[178, 24, 43], [214, 96, 77], [244, 165, 130], [247, 247, 247],
|
|
54
|
+
[146, 197, 222], [67, 147, 195], [33, 102, 172],
|
|
55
|
+
], dtype=float)
|
|
56
|
+
|
|
57
|
+
# Spectral: red-orange-yellow-green-blue diverging (ColorBrewer).
|
|
58
|
+
_SPECTRAL_ANCHORS = np.array([
|
|
59
|
+
[213, 62, 79], [252, 141, 89], [254, 224, 139], [255, 255, 191],
|
|
60
|
+
[230, 245, 152], [153, 213, 148], [50, 136, 189],
|
|
61
|
+
], dtype=float)
|
|
62
|
+
|
|
63
|
+
# PiYG: pink-white-green diverging (ColorBrewer).
|
|
64
|
+
_PIYG_ANCHORS = np.array([
|
|
65
|
+
[197, 27, 125], [233, 163, 201], [253, 224, 239], [247, 247, 247],
|
|
66
|
+
[230, 245, 208], [161, 215, 106], [77, 146, 33],
|
|
67
|
+
], dtype=float)
|
|
68
|
+
|
|
69
|
+
# BrBG: brown-white-teal diverging (ColorBrewer).
|
|
70
|
+
_BRBG_ANCHORS = np.array([
|
|
71
|
+
[140, 81, 10], [216, 179, 101], [246, 232, 195], [245, 245, 245],
|
|
72
|
+
[199, 234, 229], [90, 180, 172], [1, 102, 94],
|
|
73
|
+
], dtype=float)
|
|
74
|
+
|
|
75
|
+
# seismic: sharp blue-white-red diverging (matplotlib), for data that pivots
|
|
76
|
+
# hard at zero rather than shading gradually through it like coolwarm/RdBu.
|
|
77
|
+
_SEISMIC_ANCHORS = np.array([
|
|
78
|
+
[0, 0, 127], [0, 0, 255], [255, 255, 255], [255, 0, 0], [127, 0, 0],
|
|
79
|
+
], dtype=float)
|
|
80
|
+
|
|
81
|
+
# Single-hue sequential family (ColorBrewer): light-to-dark, for data with no
|
|
82
|
+
# natural midpoint -- a density, a count, a magnitude.
|
|
83
|
+
_BLUES_ANCHORS = np.array([
|
|
84
|
+
[247, 251, 255], [222, 235, 247], [198, 219, 239], [158, 202, 225],
|
|
85
|
+
[107, 174, 214], [49, 130, 189], [8, 81, 156],
|
|
86
|
+
], dtype=float)
|
|
87
|
+
|
|
88
|
+
_GREENS_ANCHORS = np.array([
|
|
89
|
+
[247, 252, 245], [229, 245, 224], [199, 233, 192], [161, 217, 155],
|
|
90
|
+
[116, 196, 118], [49, 163, 84], [0, 109, 44],
|
|
91
|
+
], dtype=float)
|
|
92
|
+
|
|
93
|
+
_ORANGES_ANCHORS = np.array([
|
|
94
|
+
[255, 245, 235], [254, 230, 206], [253, 208, 162], [253, 174, 107],
|
|
95
|
+
[253, 141, 60], [230, 85, 13], [166, 54, 3],
|
|
96
|
+
], dtype=float)
|
|
97
|
+
|
|
98
|
+
_REDS_ANCHORS = np.array([
|
|
99
|
+
[255, 245, 240], [254, 224, 210], [252, 187, 161], [252, 146, 114],
|
|
100
|
+
[251, 106, 74], [222, 45, 38], [165, 15, 21],
|
|
101
|
+
], dtype=float)
|
|
102
|
+
|
|
103
|
+
_PURPLES_ANCHORS = np.array([
|
|
104
|
+
[252, 251, 253], [239, 237, 245], [218, 218, 235], [188, 189, 220],
|
|
105
|
+
[158, 154, 200], [117, 107, 177], [84, 39, 143],
|
|
106
|
+
], dtype=float)
|
|
107
|
+
|
|
108
|
+
# YlOrRd: yellow-orange-red sequential (ColorBrewer) -- a common heatmap map.
|
|
109
|
+
_YLORRD_ANCHORS = np.array([
|
|
110
|
+
[255, 255, 178], [254, 217, 118], [254, 178, 76], [253, 141, 60],
|
|
111
|
+
[252, 78, 42], [227, 26, 28], [177, 0, 38],
|
|
112
|
+
], dtype=float)
|
|
113
|
+
|
|
114
|
+
# twilight: cyclic (matplotlib) -- the first and last anchor match, so it
|
|
115
|
+
# wraps cleanly for data with no true minimum/maximum, like a phase or angle.
|
|
116
|
+
_TWILIGHT_ANCHORS = np.array([
|
|
117
|
+
[23, 22, 25], [76, 66, 127], [152, 137, 192], [223, 206, 208],
|
|
118
|
+
[219, 159, 150], [164, 88, 79], [97, 46, 48], [23, 22, 25],
|
|
119
|
+
], dtype=float)
|
|
120
|
+
|
|
121
|
+
# jet: the classic MATLAB/matplotlib rainbow map. Not perceptually uniform
|
|
122
|
+
# (it implies edges in the data where its own hue turns sharply, brightest at
|
|
123
|
+
# cyan/yellow -- see docs/scale/limitations' colormap-uniformity example) but
|
|
124
|
+
# kept for the code that still asks for it by name.
|
|
125
|
+
_JET_ANCHORS = np.array([
|
|
126
|
+
[0, 0, 128], [0, 0, 255], [0, 255, 255], [0, 255, 0],
|
|
127
|
+
[255, 255, 0], [255, 0, 0], [128, 0, 0],
|
|
128
|
+
], dtype=float)
|
|
129
|
+
|
|
130
|
+
# turbo: Google's perceptually-improved rainbow -- similar use case to jet
|
|
131
|
+
# (a wide, intuitively-ordered hue sweep) without jet's flat middle band or
|
|
132
|
+
# its hard clipping at black/dark red.
|
|
133
|
+
_TURBO_ANCHORS = np.array([
|
|
134
|
+
[48, 18, 59], [63, 71, 204], [40, 142, 222], [26, 187, 156],
|
|
135
|
+
[64, 209, 72], [170, 222, 36], [247, 182, 32], [230, 86, 20],
|
|
136
|
+
[122, 4, 3],
|
|
137
|
+
], dtype=float)
|
|
138
|
+
|
|
139
|
+
# hot: black-red-yellow-white -- a thermal/blackbody-radiation ramp.
|
|
140
|
+
_HOT_ANCHORS = np.array([
|
|
141
|
+
[0, 0, 0], [255, 0, 0], [255, 255, 0], [255, 255, 255],
|
|
142
|
+
], dtype=float)
|
|
143
|
+
|
|
144
|
+
# cool: a two-stop cyan-to-magenta linear ramp.
|
|
145
|
+
_COOL_ANCHORS = np.array([
|
|
146
|
+
[0, 255, 255], [255, 0, 255],
|
|
147
|
+
], dtype=float)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _build_lut(anchors: np.ndarray, n: int = 256) -> np.ndarray:
|
|
151
|
+
"""Linearly interpolate anchor stops into an ``(n, 3)`` uint8 LUT."""
|
|
152
|
+
m = anchors.shape[0]
|
|
153
|
+
src = np.linspace(0.0, 1.0, m)
|
|
154
|
+
dst = np.linspace(0.0, 1.0, n)
|
|
155
|
+
lut = np.empty((n, 3), dtype=np.uint8)
|
|
156
|
+
for c in range(3):
|
|
157
|
+
lut[:, c] = np.round(np.interp(dst, src, anchors[:, c])).astype(np.uint8)
|
|
158
|
+
return lut
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_GRAY_LUT = np.repeat(np.linspace(0, 255, 256, dtype=np.uint8)[:, None], 3, axis=1)
|
|
162
|
+
|
|
163
|
+
_COLORMAPS = {
|
|
164
|
+
# Perceptually uniform sequential
|
|
165
|
+
"viridis": _build_lut(_VIRIDIS_ANCHORS),
|
|
166
|
+
"plasma": _build_lut(_PLASMA_ANCHORS),
|
|
167
|
+
"inferno": _build_lut(_INFERNO_ANCHORS),
|
|
168
|
+
"magma": _build_lut(_MAGMA_ANCHORS),
|
|
169
|
+
"cividis": _build_lut(_CIVIDIS_ANCHORS),
|
|
170
|
+
# Single-hue / heat sequential
|
|
171
|
+
"gray": _GRAY_LUT,
|
|
172
|
+
"grey": _GRAY_LUT,
|
|
173
|
+
"Blues": _build_lut(_BLUES_ANCHORS),
|
|
174
|
+
"Greens": _build_lut(_GREENS_ANCHORS),
|
|
175
|
+
"Oranges": _build_lut(_ORANGES_ANCHORS),
|
|
176
|
+
"Reds": _build_lut(_REDS_ANCHORS),
|
|
177
|
+
"Purples": _build_lut(_PURPLES_ANCHORS),
|
|
178
|
+
"YlOrRd": _build_lut(_YLORRD_ANCHORS),
|
|
179
|
+
"hot": _build_lut(_HOT_ANCHORS),
|
|
180
|
+
# Diverging
|
|
181
|
+
"coolwarm": _build_lut(_COOLWARM_ANCHORS),
|
|
182
|
+
"RdBu": _build_lut(_RDBU_ANCHORS),
|
|
183
|
+
"Spectral": _build_lut(_SPECTRAL_ANCHORS),
|
|
184
|
+
"PiYG": _build_lut(_PIYG_ANCHORS),
|
|
185
|
+
"BrBG": _build_lut(_BRBG_ANCHORS),
|
|
186
|
+
"seismic": _build_lut(_SEISMIC_ANCHORS),
|
|
187
|
+
# Cyclic
|
|
188
|
+
"twilight": _build_lut(_TWILIGHT_ANCHORS),
|
|
189
|
+
# Miscellaneous / rainbow
|
|
190
|
+
"jet": _build_lut(_JET_ANCHORS),
|
|
191
|
+
"turbo": _build_lut(_TURBO_ANCHORS),
|
|
192
|
+
"cool": _build_lut(_COOL_ANCHORS),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_cmap(name) -> np.ndarray:
|
|
197
|
+
"""Return a 256x3 uint8 LUT for ``name`` (or pass an LUT through).
|
|
198
|
+
|
|
199
|
+
A trailing ``_r`` reverses any known map, e.g. ``"viridis_r"`` -- matching
|
|
200
|
+
matplotlib's reversed-colormap convention.
|
|
201
|
+
"""
|
|
202
|
+
if isinstance(name, np.ndarray):
|
|
203
|
+
return name
|
|
204
|
+
key, reverse = name, False
|
|
205
|
+
if isinstance(name, str) and name.endswith("_r"):
|
|
206
|
+
key, reverse = name[:-2], True
|
|
207
|
+
try:
|
|
208
|
+
lut = _COLORMAPS[key]
|
|
209
|
+
except KeyError:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Unknown colormap {name!r}. Available: {available_colormaps()}"
|
|
212
|
+
)
|
|
213
|
+
return lut[::-1].copy() if reverse else lut
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def available_colormaps():
|
|
217
|
+
"""Named colormaps, including the ``_r`` reversed variants."""
|
|
218
|
+
base = sorted(_COLORMAPS)
|
|
219
|
+
return base + [n + "_r" for n in base]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Common named colors (the full CSS4/matplotlib named-color set, plus
|
|
223
|
+
# matplotlib's single-letter aliases), so both the SVG and raster/PDF
|
|
224
|
+
# backends accept any name a matplotlib user would reach for -- "crimson",
|
|
225
|
+
# "cornflowerblue" -- not just the handful of X11 basics. SVG understands
|
|
226
|
+
# CSS names natively, which used to mask this: a name outside a small table
|
|
227
|
+
# still rendered fine in the SVG backend (the browser resolved it), while
|
|
228
|
+
# the raster backend's own hex parser crashed on the exact same name with a
|
|
229
|
+
# confusing ``int(..., 16)`` error pointing nowhere near the real cause.
|
|
230
|
+
NAMED_COLORS = {
|
|
231
|
+
"red": "#ff0000", "green": "#008000", "blue": "#0000ff",
|
|
232
|
+
"black": "#000000", "white": "#ffffff", "gray": "#808080",
|
|
233
|
+
"grey": "#808080", "orange": "#ffa500", "purple": "#800080",
|
|
234
|
+
"brown": "#a52a2a", "pink": "#ffc0cb", "cyan": "#00ffff",
|
|
235
|
+
"magenta": "#ff00ff", "yellow": "#ffff00", "lime": "#00ff00",
|
|
236
|
+
"navy": "#000080", "teal": "#008080", "olive": "#808000",
|
|
237
|
+
"maroon": "#800000", "silver": "#c0c0c0", "gold": "#ffd700",
|
|
238
|
+
# matplotlib single-letter base colors
|
|
239
|
+
"b": "#0000ff", "g": "#008000", "r": "#ff0000", "c": "#00bfbf",
|
|
240
|
+
"m": "#bf00bf", "y": "#bfbf00", "k": "#000000", "w": "#ffffff",
|
|
241
|
+
# The rest of the CSS4 named-color set (matplotlib.colors.CSS4_COLORS).
|
|
242
|
+
"aliceblue": "#F0F8FF", "antiquewhite": "#FAEBD7", "aqua": "#00FFFF",
|
|
243
|
+
"aquamarine": "#7FFFD4", "azure": "#F0FFFF", "beige": "#F5F5DC", "bisque": "#FFE4C4",
|
|
244
|
+
"blanchedalmond": "#FFEBCD", "blueviolet": "#8A2BE2", "burlywood": "#DEB887",
|
|
245
|
+
"cadetblue": "#5F9EA0", "chartreuse": "#7FFF00", "chocolate": "#D2691E",
|
|
246
|
+
"coral": "#FF7F50", "cornflowerblue": "#6495ED", "cornsilk": "#FFF8DC",
|
|
247
|
+
"crimson": "#DC143C", "darkblue": "#00008B", "darkcyan": "#008B8B",
|
|
248
|
+
"darkgoldenrod": "#B8860B", "darkgray": "#A9A9A9", "darkgreen": "#006400",
|
|
249
|
+
"darkgrey": "#A9A9A9", "darkkhaki": "#BDB76B", "darkmagenta": "#8B008B",
|
|
250
|
+
"darkolivegreen": "#556B2F", "darkorange": "#FF8C00", "darkorchid": "#9932CC",
|
|
251
|
+
"darkred": "#8B0000", "darksalmon": "#E9967A", "darkseagreen": "#8FBC8F",
|
|
252
|
+
"darkslateblue": "#483D8B", "darkslategray": "#2F4F4F", "darkslategrey": "#2F4F4F",
|
|
253
|
+
"darkturquoise": "#00CED1", "darkviolet": "#9400D3", "deeppink": "#FF1493",
|
|
254
|
+
"deepskyblue": "#00BFFF", "dimgray": "#696969", "dimgrey": "#696969",
|
|
255
|
+
"dodgerblue": "#1E90FF", "firebrick": "#B22222", "floralwhite": "#FFFAF0",
|
|
256
|
+
"forestgreen": "#228B22", "fuchsia": "#FF00FF", "gainsboro": "#DCDCDC",
|
|
257
|
+
"ghostwhite": "#F8F8FF", "goldenrod": "#DAA520", "greenyellow": "#ADFF2F",
|
|
258
|
+
"honeydew": "#F0FFF0", "hotpink": "#FF69B4", "indianred": "#CD5C5C", "indigo": "#4B0082",
|
|
259
|
+
"ivory": "#FFFFF0", "khaki": "#F0E68C", "lavender": "#E6E6FA",
|
|
260
|
+
"lavenderblush": "#FFF0F5", "lawngreen": "#7CFC00", "lemonchiffon": "#FFFACD",
|
|
261
|
+
"lightblue": "#ADD8E6", "lightcoral": "#F08080", "lightcyan": "#E0FFFF",
|
|
262
|
+
"lightgoldenrodyellow": "#FAFAD2", "lightgray": "#D3D3D3", "lightgreen": "#90EE90",
|
|
263
|
+
"lightgrey": "#D3D3D3", "lightpink": "#FFB6C1", "lightsalmon": "#FFA07A",
|
|
264
|
+
"lightseagreen": "#20B2AA", "lightskyblue": "#87CEFA", "lightslategray": "#778899",
|
|
265
|
+
"lightslategrey": "#778899", "lightsteelblue": "#B0C4DE", "lightyellow": "#FFFFE0",
|
|
266
|
+
"limegreen": "#32CD32", "linen": "#FAF0E6", "mediumaquamarine": "#66CDAA",
|
|
267
|
+
"mediumblue": "#0000CD", "mediumorchid": "#BA55D3", "mediumpurple": "#9370DB",
|
|
268
|
+
"mediumseagreen": "#3CB371", "mediumslateblue": "#7B68EE",
|
|
269
|
+
"mediumspringgreen": "#00FA9A", "mediumturquoise": "#48D1CC",
|
|
270
|
+
"mediumvioletred": "#C71585", "midnightblue": "#191970", "mintcream": "#F5FFFA",
|
|
271
|
+
"mistyrose": "#FFE4E1", "moccasin": "#FFE4B5", "navajowhite": "#FFDEAD",
|
|
272
|
+
"oldlace": "#FDF5E6", "olivedrab": "#6B8E23", "orangered": "#FF4500",
|
|
273
|
+
"orchid": "#DA70D6", "palegoldenrod": "#EEE8AA", "palegreen": "#98FB98",
|
|
274
|
+
"paleturquoise": "#AFEEEE", "palevioletred": "#DB7093", "papayawhip": "#FFEFD5",
|
|
275
|
+
"peachpuff": "#FFDAB9", "peru": "#CD853F", "plum": "#DDA0DD", "powderblue": "#B0E0E6",
|
|
276
|
+
"rebeccapurple": "#663399", "rosybrown": "#BC8F8F", "royalblue": "#4169E1",
|
|
277
|
+
"saddlebrown": "#8B4513", "salmon": "#FA8072", "sandybrown": "#F4A460",
|
|
278
|
+
"seagreen": "#2E8B57", "seashell": "#FFF5EE", "sienna": "#A0522D", "skyblue": "#87CEEB",
|
|
279
|
+
"slateblue": "#6A5ACD", "slategray": "#708090", "slategrey": "#708090",
|
|
280
|
+
"snow": "#FFFAFA", "springgreen": "#00FF7F", "steelblue": "#4682B4", "tan": "#D2B48C",
|
|
281
|
+
"thistle": "#D8BFD8", "tomato": "#FF6347", "turquoise": "#40E0D0", "violet": "#EE82EE",
|
|
282
|
+
"wheat": "#F5DEB3", "whitesmoke": "#F5F5F5", "yellowgreen": "#9ACD32",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#: SVG/CSS paint keywords that are deliberately *not* colors -- passed
|
|
286
|
+
#: through as-is rather than resolved. ``_BBOX_DEFAULTS["edgecolor"]`` and
|
|
287
|
+
#: several call sites use ``"none"`` as "draw nothing"; ``"transparent"`` is
|
|
288
|
+
#: the equivalent CSS keyword a caller might reach for instead.
|
|
289
|
+
_PAINT_KEYWORDS = frozenset(("none", "transparent"))
|
|
290
|
+
|
|
291
|
+
_HEX_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def to_hex(color) -> str:
|
|
295
|
+
"""Resolve a color name -- or a matplotlib-style RGB(A) tuple -- to
|
|
296
|
+
``#rrggbb``; pass hex (and the ``"none"``/``"transparent"`` paint
|
|
297
|
+
keywords) through unchanged.
|
|
298
|
+
|
|
299
|
+
A flat, purely-numeric 3- or 4-element sequence (``(1.0, 0.0, 0.0)``,
|
|
300
|
+
``(255, 128, 0)``) is treated as a single RGB(A) color and converted --
|
|
301
|
+
this used to pass straight through unresolved, reaching the SVG backend
|
|
302
|
+
as a literal, invalid ``stroke="(1.0, 0.0, 0.0)"`` (silently invisible:
|
|
303
|
+
a browser treats an unrecognized value as unset) or the raster
|
|
304
|
+
backend's hex parser, which failed with ``'tuple' object has no
|
|
305
|
+
attribute 'lstrip'``, never mentioning color was the problem. Anything
|
|
306
|
+
else non-string -- notably a *nested* per-item array/list, the shape
|
|
307
|
+
``bar(color=[[r,g,b,a], ...])``'s one-color-per-bar path uses -- is left
|
|
308
|
+
exactly alone: resolving each of *those* colors individually is that
|
|
309
|
+
caller's own job (see ``artists._as_colors``), not this function's.
|
|
310
|
+
|
|
311
|
+
Raises ``ValueError`` only for a string that resolves to neither a
|
|
312
|
+
known name nor valid hex -- a misspelled name (``"crimon"``) or
|
|
313
|
+
malformed hex (``"#zzzzzz"``).
|
|
314
|
+
"""
|
|
315
|
+
if color is None:
|
|
316
|
+
return color
|
|
317
|
+
if not isinstance(color, str):
|
|
318
|
+
arr = np.asarray(color)
|
|
319
|
+
if arr.ndim == 1 and arr.size in (3, 4) and arr.dtype.kind in "iuf":
|
|
320
|
+
rgb = arr[:3].astype(float)
|
|
321
|
+
if rgb.max() <= 1.0:
|
|
322
|
+
rgb = rgb * 255
|
|
323
|
+
r, g, b = (int(round(v)) for v in rgb)
|
|
324
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
325
|
+
return color
|
|
326
|
+
if color.lower() in _PAINT_KEYWORDS:
|
|
327
|
+
return color
|
|
328
|
+
if color.startswith("#"):
|
|
329
|
+
if not _HEX_RE.match(color):
|
|
330
|
+
raise ValueError(
|
|
331
|
+
f"Invalid hex color {color!r} -- expected '#rgb' or '#rrggbb'."
|
|
332
|
+
)
|
|
333
|
+
return color
|
|
334
|
+
resolved = NAMED_COLORS.get(color.lower())
|
|
335
|
+
if resolved is None:
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"Unknown color {color!r}. Use a '#rrggbb'/'#rgb' hex code, an "
|
|
338
|
+
"RGB(A) tuple, or a named CSS color (e.g. 'crimson', 'steelblue')."
|
|
339
|
+
)
|
|
340
|
+
return resolved
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Normalize:
|
|
344
|
+
"""Linearly map data to [0, 1] using ``vmin``/``vmax``.
|
|
345
|
+
|
|
346
|
+
Unset limits are inferred from the data on first use. That inference writes
|
|
347
|
+
back to the instance, so artists take a private copy (see
|
|
348
|
+
:func:`resolve_norm`) rather than scaling the norm you handed them -- one
|
|
349
|
+
norm passed to two figures would otherwise pin the second to the first's
|
|
350
|
+
data range.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, vmin=None, vmax=None):
|
|
354
|
+
self.vmin = vmin
|
|
355
|
+
self.vmax = vmax
|
|
356
|
+
|
|
357
|
+
def autoscale_none(self, A):
|
|
358
|
+
"""Fill unset limits from the data, tolerating a fully masked field.
|
|
359
|
+
|
|
360
|
+
A frame where every cell is ``nan`` is a real case, not a mistake -- a
|
|
361
|
+
detector exposure that failed quality control, a tile with no coverage,
|
|
362
|
+
one panel of a stack that a shared norm still has to accept. NumPy's
|
|
363
|
+
``nanmin`` warns on an all-NaN slice and returns NaN, which then
|
|
364
|
+
propagated into the transform; fall back to a unit range instead, since
|
|
365
|
+
there is nothing to scale and every cell will be drawn transparent.
|
|
366
|
+
"""
|
|
367
|
+
A = np.asarray(A, dtype=float)
|
|
368
|
+
finite = A[np.isfinite(A)] if A.size else A
|
|
369
|
+
if self.vmin is None:
|
|
370
|
+
self.vmin = float(finite.min()) if finite.size else 0.0
|
|
371
|
+
if self.vmax is None:
|
|
372
|
+
self.vmax = float(finite.max()) if finite.size else 1.0
|
|
373
|
+
|
|
374
|
+
def __call__(self, A):
|
|
375
|
+
A = np.asarray(A, dtype=float)
|
|
376
|
+
self.autoscale_none(A)
|
|
377
|
+
span = self.vmax - self.vmin
|
|
378
|
+
if span == 0:
|
|
379
|
+
span = 1.0
|
|
380
|
+
return (A - self.vmin) / span
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class LogNorm(Normalize):
|
|
384
|
+
"""Map data to [0, 1] on a **log10** scale between ``vmin`` and ``vmax``.
|
|
385
|
+
|
|
386
|
+
Non-positive values map to NaN (rendered transparent, like matplotlib's
|
|
387
|
+
masked handling). Unset limits are inferred from the positive data.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def autoscale_none(self, A):
|
|
391
|
+
A = np.asarray(A, dtype=float)
|
|
392
|
+
if self.vmin is None or self.vmax is None:
|
|
393
|
+
pos = A[np.isfinite(A) & (A > 0)]
|
|
394
|
+
if self.vmin is None:
|
|
395
|
+
self.vmin = float(pos.min()) if pos.size else 1e-10
|
|
396
|
+
if self.vmax is None:
|
|
397
|
+
self.vmax = float(pos.max()) if pos.size else 1.0
|
|
398
|
+
|
|
399
|
+
def __call__(self, A):
|
|
400
|
+
A = np.asarray(A, dtype=float)
|
|
401
|
+
self.autoscale_none(A)
|
|
402
|
+
vmin = max(self.vmin, 1e-300)
|
|
403
|
+
lmin, lmax = np.log10(vmin), np.log10(max(self.vmax, vmin * 10))
|
|
404
|
+
span = (lmax - lmin) or 1.0
|
|
405
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
406
|
+
logA = np.log10(np.where(A > 0, A, np.nan))
|
|
407
|
+
return (logA - lmin) / span
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class PowerNorm(Normalize):
|
|
411
|
+
"""Map data to [0, 1] then raise to ``gamma`` (matplotlib's PowerNorm).
|
|
412
|
+
|
|
413
|
+
``gamma < 1`` emphasizes low values, ``gamma > 1`` the high end.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
def __init__(self, gamma=1.0, vmin=None, vmax=None):
|
|
417
|
+
super().__init__(vmin, vmax)
|
|
418
|
+
self.gamma = float(gamma)
|
|
419
|
+
|
|
420
|
+
def __call__(self, A):
|
|
421
|
+
A = np.asarray(A, dtype=float)
|
|
422
|
+
self.autoscale_none(A)
|
|
423
|
+
span = (self.vmax - self.vmin) or 1.0
|
|
424
|
+
t = np.clip((A - self.vmin) / span, 0.0, 1.0)
|
|
425
|
+
return np.power(t, self.gamma)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class SymLogNorm(Normalize):
|
|
429
|
+
"""Symmetric-log mapping: linear within ``+/-linthresh``, log beyond.
|
|
430
|
+
|
|
431
|
+
Handles data spanning zero and both signs (matplotlib's SymLogNorm).
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
def __init__(self, linthresh, vmin=None, vmax=None):
|
|
435
|
+
super().__init__(vmin, vmax)
|
|
436
|
+
self.linthresh = float(linthresh)
|
|
437
|
+
|
|
438
|
+
def _symlog(self, x):
|
|
439
|
+
lt = self.linthresh
|
|
440
|
+
x = np.asarray(x, dtype=float)
|
|
441
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
442
|
+
far = np.sign(x) * (1.0 + np.log10(np.abs(x) / lt))
|
|
443
|
+
return np.where(np.abs(x) <= lt, x / lt, far)
|
|
444
|
+
|
|
445
|
+
def __call__(self, A):
|
|
446
|
+
A = np.asarray(A, dtype=float)
|
|
447
|
+
self.autoscale_none(A)
|
|
448
|
+
lo, hi = self._symlog(self.vmin), self._symlog(self.vmax)
|
|
449
|
+
span = (hi - lo) or 1.0
|
|
450
|
+
return (self._symlog(A) - lo) / span
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def resolve_norm(norm, vmin=None, vmax=None) -> Normalize:
|
|
454
|
+
"""Return the norm instance an artist should own.
|
|
455
|
+
|
|
456
|
+
A caller-supplied norm is *copied*. Autoscaling mutates ``vmin``/``vmax`` in
|
|
457
|
+
place, so without this the first artist to use a norm would pin it for every
|
|
458
|
+
later one -- including artists on a different figure, which is exactly the
|
|
459
|
+
shared mutable state this library sets out not to have. Copying keeps the
|
|
460
|
+
caller's object pristine and makes each artist's scaling depend only on its
|
|
461
|
+
own data.
|
|
462
|
+
|
|
463
|
+
Limits set explicitly on the norm survive the copy, so passing one
|
|
464
|
+
``Normalize(0, 100)`` to several artists still puts them on a common scale.
|
|
465
|
+
"""
|
|
466
|
+
if norm is None:
|
|
467
|
+
return Normalize(vmin, vmax)
|
|
468
|
+
return copy.copy(norm)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def colorbar_ticks(norm):
|
|
472
|
+
"""Tick ``(values, fractions, labels)`` for a colorbar honoring ``norm``.
|
|
473
|
+
|
|
474
|
+
The gradient strip is an even colormap ramp; ticks are positioned at
|
|
475
|
+
``norm(value)`` (their fractional height), so a ``LogNorm``/``PowerNorm``/
|
|
476
|
+
``SymLogNorm`` colorbar places its labels correctly instead of linearly.
|
|
477
|
+
"""
|
|
478
|
+
from .ticker import format_ticks, log_ticks, nice_ticks
|
|
479
|
+
|
|
480
|
+
vmin, vmax = norm.vmin, norm.vmax
|
|
481
|
+
vals = log_ticks(vmin, vmax) if isinstance(norm, LogNorm) else nice_ticks(vmin, vmax)
|
|
482
|
+
vals = np.asarray(vals, dtype=float)
|
|
483
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
484
|
+
fracs = np.asarray(norm(vals), dtype=float)
|
|
485
|
+
keep = np.isfinite(fracs) & (fracs >= -1e-9) & (fracs <= 1 + 1e-9)
|
|
486
|
+
vals, fracs = vals[keep], np.clip(fracs[keep], 0.0, 1.0)
|
|
487
|
+
return vals, fracs, format_ticks(vals)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def apply_colormap(A, lut, norm: Normalize) -> np.ndarray:
|
|
491
|
+
"""Map data array ``A`` to an RGBA uint8 array. NaNs become transparent."""
|
|
492
|
+
normed = norm(A)
|
|
493
|
+
finite = np.isfinite(normed)
|
|
494
|
+
idx = np.clip(np.nan_to_num(normed) * (lut.shape[0] - 1), 0, lut.shape[0] - 1)
|
|
495
|
+
idx = idx.astype(np.intp)
|
|
496
|
+
rgb = lut[idx]
|
|
497
|
+
alpha = np.where(finite, 255, 0).astype(np.uint8)
|
|
498
|
+
return np.concatenate([rgb, alpha[..., None]], axis=-1)
|