plot-io 0.1.0__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.
- plot_io/__init__.py +66 -0
- plot_io/_backend.py +114 -0
- plot_io/axis.py +574 -0
- plot_io/colorbar.py +342 -0
- plot_io/decorations.py +496 -0
- plot_io/defaults.py +766 -0
- plot_io/density.py +97 -0
- plot_io/enums.py +63 -0
- plot_io/facets.py +184 -0
- plot_io/figure.py +331 -0
- plot_io/labels.py +63 -0
- plot_io/legend.py +498 -0
- plot_io/limits.py +92 -0
- plot_io/maps/__init__.py +365 -0
- plot_io/maps/data.py +333 -0
- plot_io/markers.py +117 -0
- plot_io/numbers.py +289 -0
- plot_io/occupancy.py +219 -0
- plot_io/palettes.py +142 -0
- plot_io/plot.py +435 -0
- plot_io/render.py +1122 -0
- plot_io/scales.py +93 -0
- plot_io/series/__init__.py +498 -0
- plot_io/series/area.py +181 -0
- plot_io/series/bar.py +294 -0
- plot_io/series/box.py +401 -0
- plot_io/series/catdata.py +23 -0
- plot_io/series/catpoints.py +182 -0
- plot_io/series/contour.py +294 -0
- plot_io/series/curves.py +128 -0
- plot_io/series/ecdf.py +143 -0
- plot_io/series/errorbar.py +111 -0
- plot_io/series/errors.py +100 -0
- plot_io/series/fill.py +168 -0
- plot_io/series/heatmap.py +304 -0
- plot_io/series/hexbin.py +216 -0
- plot_io/series/hist.py +379 -0
- plot_io/series/hist2d.py +163 -0
- plot_io/series/kde.py +228 -0
- plot_io/series/kde2d.py +153 -0
- plot_io/series/line.py +173 -0
- plot_io/series/pie.py +304 -0
- plot_io/series/points.py +341 -0
- plot_io/series/regression.py +250 -0
- plot_io/series/scatter.py +103 -0
- plot_io/series/stem.py +112 -0
- plot_io/series/violin.py +321 -0
- plot_io/settings.py +281 -0
- plot_io/shapes.py +57 -0
- plot_io/sharing.py +160 -0
- plot_io/slots.py +52 -0
- plot_io/stacking.py +63 -0
- plot_io/stats.py +140 -0
- plot_io/style.py +71 -0
- plot_io/styles/__init__.py +234 -0
- plot_io/styles/library.py +732 -0
- plot_io/tables.py +141 -0
- plot_io/text.py +109 -0
- plot_io/ticks.py +284 -0
- plot_io/times.py +199 -0
- plot_io/values.py +161 -0
- plot_io-0.1.0.dist-info/METADATA +102 -0
- plot_io-0.1.0.dist-info/RECORD +65 -0
- plot_io-0.1.0.dist-info/WHEEL +4 -0
- plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/maps/__init__.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Color and size maps.
|
|
2
|
+
|
|
3
|
+
A :class:`ColorMap` is a color gradient plus the mapping of values onto it (limits and
|
|
4
|
+
scale); a :class:`SizeMap` maps values to marker diameters. Values stay on the series
|
|
5
|
+
(``color_values``, ``size_values``), so one map can serve several series.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pdf_io.colors import to_rgba
|
|
13
|
+
|
|
14
|
+
from .. import _backend
|
|
15
|
+
from ..limits import check_limits
|
|
16
|
+
from ..scales import NUMERIC_SCALES
|
|
17
|
+
from ..settings import one_of
|
|
18
|
+
from .data import DATA
|
|
19
|
+
|
|
20
|
+
__all__ = ["ColorMap", "SizeMap", "register_color_map", "color_map_names"]
|
|
21
|
+
|
|
22
|
+
RGBA = tuple[float, float, float, float]
|
|
23
|
+
_check_scale = one_of(*NUMERIC_SCALES)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Oklab
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _to_linear(c: float) -> float:
|
|
31
|
+
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _from_linear(c: float) -> float:
|
|
35
|
+
c = min(1.0, max(0.0, c))
|
|
36
|
+
return 12.92 * c if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def srgb_to_oklab(r: float, g: float, b: float) -> tuple[float, float, float]:
|
|
40
|
+
r, g, b = _to_linear(r), _to_linear(g), _to_linear(b)
|
|
41
|
+
lc = (0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b) ** (1 / 3)
|
|
42
|
+
mc = (0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b) ** (1 / 3)
|
|
43
|
+
sc = (0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b) ** (1 / 3)
|
|
44
|
+
return (0.2104542553 * lc + 0.7936177850 * mc - 0.0040720468 * sc,
|
|
45
|
+
1.9779984951 * lc - 2.4285922050 * mc + 0.4505937099 * sc,
|
|
46
|
+
0.0259040371 * lc + 0.7827717662 * mc - 0.8086757660 * sc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def oklab_to_srgb(L: float, a: float, b: float) -> tuple[float, float, float]:
|
|
50
|
+
lc = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
|
|
51
|
+
mc = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
|
|
52
|
+
sc = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3
|
|
53
|
+
return (_from_linear(4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc),
|
|
54
|
+
_from_linear(-1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc),
|
|
55
|
+
_from_linear(-0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Color maps
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _rgba(color: Any) -> RGBA:
|
|
63
|
+
try:
|
|
64
|
+
c = to_rgba(color)
|
|
65
|
+
except (TypeError, ValueError):
|
|
66
|
+
c = None
|
|
67
|
+
if c is None:
|
|
68
|
+
raise ValueError(f"invalid color {color!r}")
|
|
69
|
+
return (float(c[0]), float(c[1]), float(c[2]), float(c[3]) if len(c) > 3 else 1.0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _hex_stops(data: str) -> list[tuple[float, RGBA]]:
|
|
73
|
+
colors = [(int(data[i:i + 2], 16) / 255, int(data[i + 2:i + 4], 16) / 255,
|
|
74
|
+
int(data[i + 4:i + 6], 16) / 255, 1.0) for i in range(0, len(data), 6)]
|
|
75
|
+
n = len(colors) - 1
|
|
76
|
+
return [(i / n, c) for i, c in enumerate(colors)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#: Registered maps: name → (stops, space).
|
|
80
|
+
_REGISTRY: dict[str, tuple[list[tuple[float, RGBA]], str]] = {
|
|
81
|
+
"grey": ([(0.0, (0.0, 0.0, 0.0, 1.0)), (1.0, (1.0, 1.0, 1.0, 1.0))], "srgb"),
|
|
82
|
+
}
|
|
83
|
+
_REGISTRY.update((name, (_hex_stops("".join(d)), "srgb")) for name, d in DATA.items())
|
|
84
|
+
#: ColorBrewer's YlGnBu and Blues (Cynthia Brewer, Apache-2.0): Vega-Lite's heatmap and ramp
|
|
85
|
+
_REGISTRY["ylgnbu"] = (_hex_stops("ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58"),
|
|
86
|
+
"srgb")
|
|
87
|
+
_REGISTRY["blues"] = (_hex_stops("f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b"),
|
|
88
|
+
"srgb")
|
|
89
|
+
#: ggplot2's continuous default (``scale_colour_gradient``), interpolated perceptually
|
|
90
|
+
_REGISTRY["ggplot2"] = ([(0.0, (0x13 / 255, 0x2B / 255, 0x43 / 255, 1.0)),
|
|
91
|
+
(1.0, (0x56 / 255, 0xB1 / 255, 0xF7 / 255, 1.0))], "oklab")
|
|
92
|
+
_ALIASES = {"gray": "grey"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def color_map_names() -> list[str]:
|
|
96
|
+
return sorted(_REGISTRY)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_color_map(name: str, color_map: Any) -> None:
|
|
100
|
+
"""Register a map under ``name`` (also usable reversed, as ``name + "_r"``)."""
|
|
101
|
+
cm = color_map if isinstance(color_map, ColorMap) else ColorMap(color_map)
|
|
102
|
+
_REGISTRY[name.strip().lower()] = (list(cm.stops), cm.space)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _lookup(name: str) -> tuple[list[tuple[float, RGBA]], str, str]:
|
|
106
|
+
key = name.strip().lower()
|
|
107
|
+
reverse = key.endswith("_r")
|
|
108
|
+
base = key[:-2] if reverse else key
|
|
109
|
+
base = _ALIASES.get(base, base)
|
|
110
|
+
if base not in _REGISTRY:
|
|
111
|
+
raise ValueError(f"unknown color map {name!r} (bundled: {', '.join(color_map_names())})")
|
|
112
|
+
stops, space = _REGISTRY[base]
|
|
113
|
+
if reverse:
|
|
114
|
+
stops = [(1.0 - p, c) for p, c in reversed(stops)]
|
|
115
|
+
return stops, space, key
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ColorMap:
|
|
119
|
+
"""A color gradient and the mapping of values onto it.
|
|
120
|
+
|
|
121
|
+
``colors``: a registered name (``"viridis"``, ``"viridis_r"``), a list of colors
|
|
122
|
+
(evenly spaced, or at ``positions``), another ``ColorMap`` (copied) or a function
|
|
123
|
+
``t -> color``. ``limits`` and ``scale`` use the axis syntax; ``"auto"`` limits cover
|
|
124
|
+
every series using the map. ``under``/``over``: colors below and above the limits
|
|
125
|
+
(default: the end colors); ``bad``: color of missing values (default: transparent).
|
|
126
|
+
``space``: interpolation between the given colors, ``"oklab"`` or ``"srgb"``.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
space: str
|
|
130
|
+
name: str | None
|
|
131
|
+
under: RGBA | None
|
|
132
|
+
over: RGBA | None
|
|
133
|
+
bad: RGBA | None
|
|
134
|
+
|
|
135
|
+
def __init__(self, colors: Any = "viridis", positions: Sequence[float] | None = None,
|
|
136
|
+
limits: Any = "auto", scale: str = "linear", under: Any = None,
|
|
137
|
+
over: Any = None, bad: Any = None, space: str | None = None,
|
|
138
|
+
name: str | None = None) -> None:
|
|
139
|
+
self.interpolation = "linear"
|
|
140
|
+
default_space = "oklab"
|
|
141
|
+
stops: list[tuple[float, RGBA]]
|
|
142
|
+
if isinstance(colors, ColorMap):
|
|
143
|
+
stops, default_space = list(colors.stops), colors.space
|
|
144
|
+
self.interpolation = colors.interpolation
|
|
145
|
+
name = name or colors.name
|
|
146
|
+
under = colors.under if under is None else under
|
|
147
|
+
over = colors.over if over is None else over
|
|
148
|
+
bad = colors.bad if bad is None else bad
|
|
149
|
+
elif isinstance(colors, str):
|
|
150
|
+
stops, default_space, key = _lookup(colors)
|
|
151
|
+
name = name or key
|
|
152
|
+
elif callable(colors):
|
|
153
|
+
stops = [(i / 255, _rgba(colors(i / 255))) for i in range(256)]
|
|
154
|
+
default_space = "srgb"
|
|
155
|
+
else:
|
|
156
|
+
cols = [_rgba(c) for c in colors]
|
|
157
|
+
if not cols:
|
|
158
|
+
raise ValueError("a color map needs at least one color")
|
|
159
|
+
if positions is None:
|
|
160
|
+
n = max(len(cols) - 1, 1)
|
|
161
|
+
stops = [(i / n, c) for i, c in enumerate(cols)]
|
|
162
|
+
else:
|
|
163
|
+
pos = [float(p) for p in positions]
|
|
164
|
+
if len(pos) != len(cols):
|
|
165
|
+
raise ValueError("give one position per color")
|
|
166
|
+
if any(b < a for a, b in zip(pos, pos[1:])) or pos[0] < 0 or pos[-1] > 1:
|
|
167
|
+
raise ValueError("positions must increase within [0, 1]")
|
|
168
|
+
stops = list(zip(pos, cols))
|
|
169
|
+
if len(stops) == 1:
|
|
170
|
+
stops = [(0.0, stops[0][1]), (1.0, stops[0][1])]
|
|
171
|
+
self.stops: list[tuple[float, RGBA]] = stops
|
|
172
|
+
self.space = one_of("oklab", "srgb")(space) if space is not None else default_space
|
|
173
|
+
self.limits = check_limits(limits)
|
|
174
|
+
self.scale = _check_scale(scale)
|
|
175
|
+
self.under = None if under is None else _rgba(under)
|
|
176
|
+
self.over = None if over is None else _rgba(over)
|
|
177
|
+
self.bad = None if bad is None else _rgba(bad)
|
|
178
|
+
self.name = name
|
|
179
|
+
self._lab = ([srgb_to_oklab(*c[:3]) for _, c in stops] if self.space == "oklab"
|
|
180
|
+
else None)
|
|
181
|
+
self._pdf: Any = None
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
return f"ColorMap({self.name or f'{len(self.stops)} colors'})"
|
|
185
|
+
|
|
186
|
+
def _copy(self, **changes: Any) -> "ColorMap":
|
|
187
|
+
args = dict(limits=self.limits, scale=self.scale, under=self.under, over=self.over,
|
|
188
|
+
bad=self.bad, space=self.space, name=self.name)
|
|
189
|
+
args.update(changes)
|
|
190
|
+
stops = changes.pop("stops", None)
|
|
191
|
+
args.pop("stops", None)
|
|
192
|
+
cm = ColorMap(self, **args)
|
|
193
|
+
cm.name = args["name"]
|
|
194
|
+
if stops is not None:
|
|
195
|
+
cm.stops = stops
|
|
196
|
+
cm._lab = ([srgb_to_oklab(*c[:3]) for _, c in stops] if cm.space == "oklab"
|
|
197
|
+
else None)
|
|
198
|
+
return cm
|
|
199
|
+
|
|
200
|
+
def __call__(self, t: float) -> RGBA:
|
|
201
|
+
"""Color at ``t`` in [0, 1] (``under``/``over`` outside, ``bad`` for NaN)."""
|
|
202
|
+
t = float(t)
|
|
203
|
+
if t != t:
|
|
204
|
+
return self.bad or (0.0, 0.0, 0.0, 0.0)
|
|
205
|
+
if t < 0:
|
|
206
|
+
return self.under or self.stops[0][1]
|
|
207
|
+
if t > 1:
|
|
208
|
+
return self.over or self.stops[-1][1]
|
|
209
|
+
stops = self.stops
|
|
210
|
+
if self.interpolation == "nearest":
|
|
211
|
+
for i in range(len(stops) - 1, -1, -1):
|
|
212
|
+
if stops[i][0] <= t:
|
|
213
|
+
return stops[i][1]
|
|
214
|
+
return stops[0][1]
|
|
215
|
+
if t <= stops[0][0]:
|
|
216
|
+
return stops[0][1]
|
|
217
|
+
if t >= stops[-1][0]:
|
|
218
|
+
return stops[-1][1]
|
|
219
|
+
lo, hi = 0, len(stops) - 1
|
|
220
|
+
while hi - lo > 1:
|
|
221
|
+
mid = (lo + hi) // 2
|
|
222
|
+
if stops[mid][0] <= t:
|
|
223
|
+
lo = mid
|
|
224
|
+
else:
|
|
225
|
+
hi = mid
|
|
226
|
+
(p0, c0), (p1, c1) = stops[lo], stops[hi]
|
|
227
|
+
f = (t - p0) / (p1 - p0) if p1 > p0 else 1.0
|
|
228
|
+
alpha = c0[3] + (c1[3] - c0[3]) * f
|
|
229
|
+
if self._lab is not None:
|
|
230
|
+
a, b = self._lab[lo], self._lab[hi]
|
|
231
|
+
r, g, bl = oklab_to_srgb(*(x + (y - x) * f for x, y in zip(a, b)))
|
|
232
|
+
return (r, g, bl, alpha)
|
|
233
|
+
return (c0[0] + (c1[0] - c0[0]) * f, c0[1] + (c1[1] - c0[1]) * f,
|
|
234
|
+
c0[2] + (c1[2] - c0[2]) * f, alpha)
|
|
235
|
+
|
|
236
|
+
def table(self, n: int = 256) -> list[RGBA]:
|
|
237
|
+
return [self(i / (n - 1)) for i in range(n)] if n > 1 else [self(0.0)]
|
|
238
|
+
|
|
239
|
+
def colors_at(self, ts: Any) -> Any:
|
|
240
|
+
"""Colors at many positions in [0, 1] (an ``(n, 4)`` buffer, or a list)."""
|
|
241
|
+
return _backend.colors_at(self.to_pdf_io(), ts)
|
|
242
|
+
|
|
243
|
+
def reversed(self) -> "ColorMap":
|
|
244
|
+
stops = [(1.0 - p, c) for p, c in reversed(self.stops)]
|
|
245
|
+
name = None
|
|
246
|
+
if self.name:
|
|
247
|
+
name = self.name[:-2] if self.name.endswith("_r") else self.name + "_r"
|
|
248
|
+
return self._copy(stops=stops, under=self.over, over=self.under, name=name)
|
|
249
|
+
|
|
250
|
+
def resampled(self, n: int) -> "ColorMap":
|
|
251
|
+
"""``n`` flat steps (a discrete map)."""
|
|
252
|
+
if n < 1:
|
|
253
|
+
raise ValueError("resampled() needs at least one step")
|
|
254
|
+
cols = [self((i + 0.5) / n) for i in range(n)]
|
|
255
|
+
cm = self._copy(stops=[(i / n, c) for i, c in enumerate(cols)], space="srgb")
|
|
256
|
+
cm.interpolation = "nearest"
|
|
257
|
+
return cm
|
|
258
|
+
|
|
259
|
+
def truncated(self, lo: float, hi: float) -> "ColorMap":
|
|
260
|
+
"""The part between ``lo`` and ``hi``, stretched to [0, 1]."""
|
|
261
|
+
if not 0 <= lo < hi <= 1:
|
|
262
|
+
raise ValueError("truncated() needs 0 <= lo < hi <= 1")
|
|
263
|
+
n = 256
|
|
264
|
+
cols = [self(lo + (hi - lo) * i / (n - 1)) for i in range(n)]
|
|
265
|
+
return self._copy(stops=[(i / (n - 1), c) for i, c in enumerate(cols)], space="srgb",
|
|
266
|
+
name=None)
|
|
267
|
+
|
|
268
|
+
def with_extremes(self, under: Any = None, over: Any = None, bad: Any = None) -> "ColorMap":
|
|
269
|
+
return self._copy(under=under if under is not None else self.under,
|
|
270
|
+
over=over if over is not None else self.over,
|
|
271
|
+
bad=bad if bad is not None else self.bad)
|
|
272
|
+
|
|
273
|
+
def pdf_stops(self) -> list[tuple[float, RGBA]]:
|
|
274
|
+
"""Stops for pdf_io (sRGB interpolation): Oklab maps as 256 samples."""
|
|
275
|
+
if self.space == "oklab" and len(self.stops) < 256 and self.interpolation == "linear":
|
|
276
|
+
return [(i / 255, self(i / 255)) for i in range(256)]
|
|
277
|
+
return list(self.stops)
|
|
278
|
+
|
|
279
|
+
def to_pdf_io(self) -> Any:
|
|
280
|
+
"""The equivalent pdf_io color map."""
|
|
281
|
+
if self._pdf is None:
|
|
282
|
+
self._pdf = _backend.pdf_color_map(self.pdf_stops(), self.interpolation,
|
|
283
|
+
self.under, self.over, self.bad)
|
|
284
|
+
return self._pdf
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def check_cmap(v: Any) -> Any:
|
|
288
|
+
"""A ``ColorMap``, or anything its ``colors`` accepts (validated)."""
|
|
289
|
+
if isinstance(v, ColorMap):
|
|
290
|
+
return v
|
|
291
|
+
ColorMap(v)
|
|
292
|
+
return v
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def as_color_map(value: Any) -> ColorMap:
|
|
296
|
+
return value if isinstance(value, ColorMap) else ColorMap(value)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Size maps
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def _size(v: Any) -> float:
|
|
304
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)) or not v >= 0:
|
|
305
|
+
raise ValueError(f"expected a size >= 0, got {v!r}")
|
|
306
|
+
return float(v)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class SizeMap:
|
|
310
|
+
"""Values to marker diameters (pt).
|
|
311
|
+
|
|
312
|
+
``sizes``: the diameters at the two limits. ``by="area"`` moves the marker area
|
|
313
|
+
linearly between the end areas; ``by="diameter"`` moves the diameter. ``under`` and
|
|
314
|
+
``over``: sizes below and above the limits (default: the end sizes; ``"none"`` leaves
|
|
315
|
+
those points out); ``bad``: size of points with a missing value (default: left out).
|
|
316
|
+
``label``: title of its size key in legends (default: the ``size`` column name).
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(self, sizes: Sequence[float] = (2, 10), limits: Any = "auto",
|
|
320
|
+
scale: str = "linear", by: str = "area", under: Any = None,
|
|
321
|
+
over: Any = None, bad: Any = None, label: Any = None) -> None:
|
|
322
|
+
self.label = label
|
|
323
|
+
if len(sizes) != 2:
|
|
324
|
+
raise ValueError("sizes must be a pair (smallest, largest)")
|
|
325
|
+
self.sizes = (_size(sizes[0]), _size(sizes[1]))
|
|
326
|
+
self.limits = check_limits(limits)
|
|
327
|
+
self.scale = _check_scale(scale)
|
|
328
|
+
self.by = one_of("area", "diameter")(by)
|
|
329
|
+
self.under = self._extreme(under)
|
|
330
|
+
self.over = self._extreme(over)
|
|
331
|
+
self.bad = self._extreme(bad)
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _extreme(v: Any) -> Any:
|
|
335
|
+
if v is None:
|
|
336
|
+
return None
|
|
337
|
+
if isinstance(v, str) and v.lower() == "none":
|
|
338
|
+
return math.nan
|
|
339
|
+
return _size(v)
|
|
340
|
+
|
|
341
|
+
def __repr__(self) -> str:
|
|
342
|
+
return f"SizeMap({self.sizes[0]:g}–{self.sizes[1]:g} pt, by {self.by})"
|
|
343
|
+
|
|
344
|
+
def map(self, values: Any, flo: float, fhi: float, function: str | None) -> Any:
|
|
345
|
+
"""Diameters for ``values``, with ``flo``/``fhi`` the limits after the scale
|
|
346
|
+
function."""
|
|
347
|
+
d0, d1 = self.sizes
|
|
348
|
+
span = fhi - flo
|
|
349
|
+
nan = math.nan
|
|
350
|
+
under = d0 if self.under is None else self.under
|
|
351
|
+
over = d1 if self.over is None else self.over
|
|
352
|
+
bad = nan if self.bad is None else self.bad
|
|
353
|
+
if self.by == "diameter":
|
|
354
|
+
k = (d1 - d0) / span if span else 0.0
|
|
355
|
+
lo, hi = (d0, d1) if d1 >= d0 else (d1, d0)
|
|
356
|
+
u, o = (under, over) if d1 >= d0 else (over, under)
|
|
357
|
+
return _backend.scaled(values, k, d0 - k * flo, function, low=lo, high=hi,
|
|
358
|
+
under=u, over=o, nan=bad)
|
|
359
|
+
a0, a1 = d0 * d0, d1 * d1
|
|
360
|
+
k = (a1 - a0) / span if span else 0.0
|
|
361
|
+
lo, hi = (a0, a1) if a1 >= a0 else (a1, a0)
|
|
362
|
+
u, o = (under * under, over * over) if a1 >= a0 else (over * over, under * under)
|
|
363
|
+
areas = _backend.scaled(values, k, a0 - k * flo, function, low=lo, high=hi,
|
|
364
|
+
under=u, over=o, nan=bad * bad)
|
|
365
|
+
return _backend.scaled(areas, 1.0, 0.0, "sqrt")
|