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.
Files changed (65) hide show
  1. plot_io/__init__.py +66 -0
  2. plot_io/_backend.py +114 -0
  3. plot_io/axis.py +574 -0
  4. plot_io/colorbar.py +342 -0
  5. plot_io/decorations.py +496 -0
  6. plot_io/defaults.py +766 -0
  7. plot_io/density.py +97 -0
  8. plot_io/enums.py +63 -0
  9. plot_io/facets.py +184 -0
  10. plot_io/figure.py +331 -0
  11. plot_io/labels.py +63 -0
  12. plot_io/legend.py +498 -0
  13. plot_io/limits.py +92 -0
  14. plot_io/maps/__init__.py +365 -0
  15. plot_io/maps/data.py +333 -0
  16. plot_io/markers.py +117 -0
  17. plot_io/numbers.py +289 -0
  18. plot_io/occupancy.py +219 -0
  19. plot_io/palettes.py +142 -0
  20. plot_io/plot.py +435 -0
  21. plot_io/render.py +1122 -0
  22. plot_io/scales.py +93 -0
  23. plot_io/series/__init__.py +498 -0
  24. plot_io/series/area.py +181 -0
  25. plot_io/series/bar.py +294 -0
  26. plot_io/series/box.py +401 -0
  27. plot_io/series/catdata.py +23 -0
  28. plot_io/series/catpoints.py +182 -0
  29. plot_io/series/contour.py +294 -0
  30. plot_io/series/curves.py +128 -0
  31. plot_io/series/ecdf.py +143 -0
  32. plot_io/series/errorbar.py +111 -0
  33. plot_io/series/errors.py +100 -0
  34. plot_io/series/fill.py +168 -0
  35. plot_io/series/heatmap.py +304 -0
  36. plot_io/series/hexbin.py +216 -0
  37. plot_io/series/hist.py +379 -0
  38. plot_io/series/hist2d.py +163 -0
  39. plot_io/series/kde.py +228 -0
  40. plot_io/series/kde2d.py +153 -0
  41. plot_io/series/line.py +173 -0
  42. plot_io/series/pie.py +304 -0
  43. plot_io/series/points.py +341 -0
  44. plot_io/series/regression.py +250 -0
  45. plot_io/series/scatter.py +103 -0
  46. plot_io/series/stem.py +112 -0
  47. plot_io/series/violin.py +321 -0
  48. plot_io/settings.py +281 -0
  49. plot_io/shapes.py +57 -0
  50. plot_io/sharing.py +160 -0
  51. plot_io/slots.py +52 -0
  52. plot_io/stacking.py +63 -0
  53. plot_io/stats.py +140 -0
  54. plot_io/style.py +71 -0
  55. plot_io/styles/__init__.py +234 -0
  56. plot_io/styles/library.py +732 -0
  57. plot_io/tables.py +141 -0
  58. plot_io/text.py +109 -0
  59. plot_io/ticks.py +284 -0
  60. plot_io/times.py +199 -0
  61. plot_io/values.py +161 -0
  62. plot_io-0.1.0.dist-info/METADATA +102 -0
  63. plot_io-0.1.0.dist-info/RECORD +65 -0
  64. plot_io-0.1.0.dist-info/WHEEL +4 -0
  65. plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/scales.py ADDED
@@ -0,0 +1,93 @@
1
+ """Scales: a monotone function from data to a position line.
2
+
3
+ A numeric scale maps a value ``v`` to ``f(v)``; an axis maps ``f(lo)…f(hi)`` linearly onto
4
+ its length. ``f`` is one of the functions pdf_io's ``arrays.scaled`` applies in C, so bulk
5
+ data never goes through a Python loop.
6
+ """
7
+
8
+ import math
9
+ from collections.abc import Callable
10
+
11
+ __all__ = ["Scale", "SCALES", "get_scale", "NUMERIC_SCALES", "AXIS_SCALES"]
12
+
13
+
14
+ class Scale:
15
+ """``forward``/``inverse`` on single values; ``function`` is pdf_io's name for it."""
16
+
17
+ __slots__ = ("name", "function", "forward", "inverse", "lo", "hi", "options")
18
+
19
+ def __init__(self, name: str, function: str | None, forward: Callable[[float], float],
20
+ inverse: Callable[[float], float], lo: float = -math.inf,
21
+ hi: float = math.inf, options: dict | None = None) -> None:
22
+ self.name = name
23
+ self.function = function
24
+ self.forward = forward
25
+ self.inverse = inverse
26
+ self.lo = lo # open domain bounds for log, closed for sqrt (see contains)
27
+ self.hi = hi
28
+ self.options = options or {} # extra arguments of pdf_io's scaled (linthresh)
29
+
30
+ def contains(self, v: float) -> bool:
31
+ if not math.isfinite(v):
32
+ return False
33
+ if self.name in ("log", "log2", "log10"):
34
+ return v > 0
35
+ if self.name == "sqrt":
36
+ return v >= 0
37
+ if self.name == "exp":
38
+ return v < 709.78
39
+ if self.name == "logit":
40
+ return 0.0 < v < 1.0
41
+ return True
42
+
43
+ def f(self, v: float) -> float:
44
+ return self.forward(v) if self.contains(v) else math.nan
45
+
46
+ def __repr__(self) -> str:
47
+ return f"Scale({self.name})"
48
+
49
+
50
+ def _safe_exp(t: float) -> float:
51
+ return math.exp(t) if t < 709.78 else math.inf
52
+
53
+
54
+ SCALES = {
55
+ "linear": Scale("linear", None, lambda v: v, lambda t: t),
56
+ "log": Scale("log", "log", math.log, _safe_exp),
57
+ "log2": Scale("log2", "log2", math.log2, lambda t: 2.0 ** t if t < 1024 else math.inf),
58
+ "log10": Scale("log10", "log10", math.log10, lambda t: 10.0 ** t if t < 308 else math.inf),
59
+ "sqrt": Scale("sqrt", "sqrt", math.sqrt, lambda t: t * t),
60
+ "exp": Scale("exp", "exp", _safe_exp, lambda t: math.log(t) if t > 0 else -math.inf),
61
+ }
62
+
63
+ def _symlog(c: float) -> Scale:
64
+ def f(v: float) -> float:
65
+ return math.copysign(math.log10(1.0 + abs(v) / c), v)
66
+
67
+ def inv(t: float) -> float:
68
+ return math.copysign(c * (10.0 ** abs(t) - 1.0), t) if abs(t) < 308 else \
69
+ math.copysign(math.inf, t)
70
+ return Scale("symlog", "symlog", f, inv, options={"linthresh": c})
71
+
72
+
73
+ def _logit_inverse(t: float) -> float:
74
+ if t > 300:
75
+ return 1.0
76
+ return 1.0 / (1.0 + 10.0 ** -t)
77
+
78
+
79
+ SCALES["symlog"] = _symlog(1.0)
80
+ SCALES["logit"] = Scale("logit", "logit", lambda p: math.log10(p / (1.0 - p)), _logit_inverse)
81
+
82
+ NUMERIC_SCALES = tuple(SCALES)
83
+ #: Scales an axis takes: the numeric ones, ``time`` (set by date data) and ``category``.
84
+ AXIS_SCALES = NUMERIC_SCALES + ("time", "category")
85
+
86
+ SCALES["time"] = Scale("time", None, lambda v: v, lambda t: t)
87
+
88
+
89
+ def get_scale(name: str, linthresh: float | None = None) -> Scale:
90
+ """The scale ``name`` (``linthresh``: the linear region of ``symlog``)."""
91
+ if name == "symlog" and linthresh is not None and linthresh != 1.0:
92
+ return _symlog(float(linthresh))
93
+ return SCALES[name]
@@ -0,0 +1,498 @@
1
+ """Series: data plus style, drawn in a plot.
2
+
3
+ A series reports what the axes need (extent, categories, values for quantile limits),
4
+ the color and size maps it uses, and draws itself and its legend glyph. New plot types
5
+ are new subclasses.
6
+ """
7
+
8
+ import math
9
+ from typing import Any
10
+
11
+ from .. import _backend
12
+ from ..settings import Setting, Settings, color, dpi_or_bool, number, route, simplify_value, table, text
13
+
14
+ __all__ = ["Series", "COMMON", "DrawContext"]
15
+
16
+ COMMON = dict(
17
+ name=lambda: Setting(text, doc="Legend entry; unset: not in the legend."),
18
+ alpha=lambda: Setting(number(0, 1), inherited=True, doc="Opacity, multiplying the colors' own."),
19
+ z=lambda: Setting(number(), doc="Drawing order (higher on top); default: call order."),
20
+ rasterize=lambda: Setting(dpi_or_bool, inherited=True,
21
+ doc="`True` or a dpi: drawn as an image inside the PDF."),
22
+ simplify=lambda: Setting(simplify_value, inherited=True,
23
+ doc="`\"auto\"`, `True`, a dpi or `False`: pdf_io's `simplify`."),
24
+ font_size=lambda: Setting(number(positive=True), inherited=True, doc="Base font size (pt).", length=True),
25
+ font_family=lambda: Setting(str, inherited=True, doc="Font family."),
26
+ text_color=lambda: Setting(color, inherited=True, doc="Text color."),
27
+ line_width=lambda: Setting(number(lo=0), inherited=True, doc="Line width (pt).", length=True),
28
+ color_map=lambda: Setting(_check_color_map, inherited=True,
29
+ doc="How numbers in `color` become colors: a `ColorMap`, a map "
30
+ "name or colors (alias `cmap`); default: the style's."),
31
+ size_legend=lambda: Setting(_size_legend,
32
+ doc="With a `size_map`: a size key in the legend (`True`), "
33
+ "none (`False`), or the values to show (a list)."),
34
+ size_map=lambda: Setting(_check_size_map,
35
+ doc="How numbers in `size` become sizes: a `SizeMap` (alias "
36
+ "`smap`); without it, numbers are sizes in pt."),
37
+ )
38
+
39
+
40
+ def _size_legend(v: Any) -> Any:
41
+ if isinstance(v, bool):
42
+ return v
43
+ if isinstance(v, (list, tuple)) and v:
44
+ return [float(x) for x in v]
45
+ raise ValueError(f"expected True, False or a list of values, got {v!r}")
46
+
47
+
48
+ def _check_color_map(v: Any) -> Any:
49
+ from ..values import check_color_map
50
+ return check_color_map(v)
51
+
52
+
53
+ def _check_size_map(v: Any) -> Any:
54
+ from ..values import check_size_map
55
+ return check_size_map(v)
56
+
57
+
58
+ #: Data arguments whose strings name columns when ``data`` is given (others than the
59
+ #: positional ones).
60
+ DATA_KEYS = ("color", "line_color", "marker_color", "size", "alpha", "group", "order",
61
+ "x_error", "y_error", "weights", "positions", "x", "y", "labels")
62
+ #: Of those, the ones whose strings must be columns.
63
+ MUST_BE_COLUMNS = ("group", "weights", "positions")
64
+ #: Per-point keyword arguments split among the members of a group.
65
+ SPLIT_KEYS = ("color", "line_color", "marker_color", "size", "alpha", "marker", "x_error",
66
+ "y_error", "order", "weights", "x", "y", "labels")
67
+
68
+
69
+ def _positional(cls: type) -> list[str]:
70
+ import inspect
71
+ names = cls.__dict__.get("_POSITIONAL")
72
+ if names is None:
73
+ sig = inspect.signature(getattr(cls, "__init__"))
74
+ names = [n for n, p in sig.parameters.items()
75
+ if n not in ("self", "plot") and p.kind in (p.POSITIONAL_ONLY,
76
+ p.POSITIONAL_OR_KEYWORD)]
77
+ setattr(cls, "_POSITIONAL", names)
78
+ return names
79
+
80
+
81
+ def _length(v: Any) -> int | None:
82
+ if v is None or isinstance(v, (str, bytes, dict, int, float)):
83
+ return None
84
+ try:
85
+ return len(v)
86
+ except TypeError:
87
+ return None
88
+
89
+
90
+ class SeriesMeta(type):
91
+ """Resolves ``data`` columns and splits ``group`` calls before a series is made."""
92
+
93
+ def __call__(cls, *args: Any, **kw: Any) -> Any:
94
+ from .. import tables
95
+ from ..settings import canonical_kwargs
96
+ with_y = getattr(cls, "with_y", None)
97
+ if with_y is not None and (len(args) == 2 or "y" in kw):
98
+ return with_y()(*args, **kw)
99
+ names = _positional(cls)
100
+ kw = canonical_kwargs(kw, getattr(cls, "SETTINGS", {}), cls.__name__)
101
+ if len(args) > len(names):
102
+ raise TypeError(f"{cls.__name__}() takes {len(names)} positional arguments")
103
+ pos = dict(zip(names, args))
104
+ for n in names:
105
+ if n in kw:
106
+ if n in pos:
107
+ raise TypeError(f"{cls.__name__}() got two values for {n!r}")
108
+ pos[n] = kw.pop(n)
109
+ plot = kw.get("plot")
110
+ data = kw.pop("data", None)
111
+ if data is None and plot is not None:
112
+ data = plot.get("data")
113
+ columns: dict[str, Any] = {}
114
+ orders: dict[str, Any] = {}
115
+ if data is not None:
116
+ for n, v in list(pos.items()):
117
+ if isinstance(v, str):
118
+ pos[n], orders[n] = tables.column(data, v)
119
+ columns[n] = v
120
+ for n in DATA_KEYS:
121
+ v = kw.get(n)
122
+ if isinstance(v, str) and n not in pos:
123
+ if tables.has_column(data, v):
124
+ if n in ("color", "line_color", "marker_color"):
125
+ from ..settings import color
126
+ try:
127
+ color(v)
128
+ tables.color_name_warning(v)
129
+ except ValueError:
130
+ pass
131
+ kw[n], orders[n] = tables.column(data, v)
132
+ columns[n] = v
133
+ elif n in MUST_BE_COLUMNS:
134
+ raise ValueError(f"no column {v!r} in data")
135
+ from .. import times as _times
136
+ time_params: dict[str, Any] = {}
137
+ for store in (pos, kw):
138
+ for n in ("x", "y", "positions"):
139
+ v = store.get(n)
140
+ if v is not None and _times.is_time_like(v):
141
+ store[n], time_params[n] = _times.to_seconds(v)
142
+ if "group" in kw:
143
+ group = cls._grouped(pos, kw, columns, orders)
144
+ for m in group:
145
+ m.times = dict(time_params)
146
+ return group
147
+ for k in ("group_order", "group_missing", "group_colors"):
148
+ if k in kw:
149
+ raise ValueError(f"{k} applies with group=")
150
+ if getattr(cls, "LONG_FORM", False):
151
+ cls._long_form(pos, kw, columns, orders)
152
+ s = super().__call__(**pos, **kw)
153
+ s.columns = columns
154
+ s.times = time_params
155
+ return s
156
+
157
+ def _long_form(cls, pos: dict[str, Any], kw: dict[str, Any], columns: dict[str, Any],
158
+ orders: dict[str, Any]) -> None:
159
+ """``values`` flat with categories per point in ``x`` (``y`` when horizontal):
160
+ a dict ``{category: values}`` in first-appearance (or categorical) order."""
161
+ key = "y" if kw.get("orientation") == "horizontal" else "x"
162
+ cats = kw.get(key)
163
+ vals = pos.get("values")
164
+ n = _length(vals)
165
+ if cats is None or n is None or isinstance(vals, dict) or _length(cats) != n:
166
+ return
167
+ kw.pop(key)
168
+ from .. import tables
169
+ out: dict[Any, list[Any]] = {}
170
+ for c in orders.get(key) or []:
171
+ out[c] = []
172
+ from .. import _backend
173
+ d = _backend.doubles(vals)
174
+ for c, v in zip(cats, d):
175
+ if tables.is_missing(c):
176
+ continue
177
+ out.setdefault(c, []).append(v)
178
+ pos["values"] = {k: v for k, v in out.items() if v}
179
+ if key in columns:
180
+ columns["categories"] = columns.pop(key)
181
+
182
+ def _grouped(cls, pos: dict[str, Any], kw: dict[str, Any], columns: dict[str, Any],
183
+ orders: dict[str, Any]) -> Any:
184
+ from .. import tables
185
+ labels = kw.pop("group")
186
+ given = kw.pop("group_order", None)
187
+ missing = kw.pop("group_missing", "keep")
188
+ if missing not in ("keep", "drop"):
189
+ raise ValueError("group_missing is 'keep' or 'drop'")
190
+ colors = kw.pop("group_colors", None) or {}
191
+ n = _length(labels)
192
+ if n is None:
193
+ raise ValueError("group needs one label per point")
194
+ if hasattr(labels, "tolist"):
195
+ labels = labels.tolist()
196
+ keys, rows = tables.group_keys(labels, orders.get("group"), given, missing)
197
+ auto_marker = kw.get("marker") == "auto"
198
+ auto_style = kw.get("line_style") == "auto"
199
+ title = columns.get("group")
200
+ members: list[Any] = []
201
+ for k, (key, idx) in enumerate(zip(keys, rows)):
202
+ pk = {nm: (tables.take(v, idx) if _length(v) == n else v) for nm, v in pos.items()}
203
+ kk = {}
204
+ for nm, v in kw.items():
205
+ if nm in SPLIT_KEYS and _length(v) == n and not (
206
+ isinstance(v, tuple) and len(v) in (3, 4)):
207
+ v = tables.take(v, idx)
208
+ kk[nm] = v
209
+ if auto_marker:
210
+ kk["marker"] = tables.MARKER_CYCLE[k % len(tables.MARKER_CYCLE)]
211
+ if auto_style:
212
+ kk["line_style"] = tables.LINE_STYLE_CYCLE[k % len(tables.LINE_STYLE_CYCLE)]
213
+ if key in colors:
214
+ kk["color"] = colors[key]
215
+ kk.setdefault("name", str(key))
216
+ if k > 0 and kk.get("plot") is None and members:
217
+ kk["plot"] = members[0].plot
218
+ s = cls(**pk, **kk)
219
+ s.columns = dict(columns)
220
+ members.append(s)
221
+ group = tables.SeriesGroup(members, keys, title)
222
+ if members and title is not None:
223
+ members[0].plot._legend_title = title
224
+ return group
225
+
226
+
227
+ #: Settings that may hold colors to map (numbers or a ``Density``).
228
+ COLOR_SETTINGS = ("color", "line_color", "marker_color")
229
+ #: Settings that may hold sizes to map.
230
+ SIZE_SETTINGS = ("size",)
231
+
232
+
233
+ def common(*names: str) -> dict[str, Setting]:
234
+ return {n: COMMON[n]() for n in names}
235
+
236
+
237
+ class DrawContext:
238
+ """What a series needs while drawing: the canvas, the axes rectangle and mappers."""
239
+
240
+ def __init__(self, canvas: Any, plot: Any, rect: tuple[float, float, float, float],
241
+ x: Any, y: Any, maps: dict[int, tuple[float, float]], palette: tuple) -> None:
242
+ self.canvas = canvas
243
+ self.plot = plot
244
+ self.rect = rect
245
+ self.x = x
246
+ self.y = y
247
+ x0, y0, x1, y1 = rect
248
+ self.xmap = x.mapper(x0, x1)
249
+ self.ymap = y.mapper(y1, y0)
250
+ self.maps = maps
251
+ self.palette = palette
252
+ self.figure_size = (x1, y1)
253
+
254
+ def color(self, value: Any) -> Any:
255
+ from ..style import resolve_color
256
+ return resolve_color(value, self.palette)
257
+
258
+ def map_limits(self, m: Any) -> tuple[float, float]:
259
+ return self.maps[id(m)]
260
+
261
+
262
+ class Series(Settings, metaclass=SeriesMeta):
263
+ """Base of every series. ``plot=None`` creates a plot (and its figure)."""
264
+
265
+ SETTINGS: dict[str, Setting] = table(**common("name", "alpha", "z", "rasterize",
266
+ "simplify"))
267
+ #: Whether the series takes the next palette color when it has no color of its own.
268
+ USES_PALETTE = True
269
+ #: Whether the series is drawn on axes (``False``: pie charts, on plots without axes).
270
+ AXES = True
271
+ #: Dodge group of categorical series sharing category slots (``"box"``, ``"bar"``,
272
+ #: ``"points"``); ``None``: not dodged.
273
+ DODGE_GROUP: str | None = None
274
+ #: Stack group of series that stack with ``stack=True`` (``"bar"``, ``"area"``).
275
+ STACK_GROUP: str | None = None
276
+
277
+ #: Whether ``values`` may be flat with categories per point in ``x`` (long data).
278
+ LONG_FORM = False
279
+
280
+ def __init__(self, *, plot: Any = None, **settings: Any) -> None:
281
+ from ..figure import Figure
282
+ from ..plot import Plot
283
+
284
+ own, plot_kw, fig_kw = route(settings, [type(self), Plot, Figure],
285
+ extra={1: Plot.EXTRA})
286
+ if plot is None:
287
+ plot = Plot(**plot_kw, **fig_kw)
288
+ else:
289
+ if plot_kw:
290
+ plot.set(**plot_kw)
291
+ if fig_kw:
292
+ plot.figure.set(**fig_kw)
293
+ self.plot = plot
294
+ self._auto_color: Any = None
295
+ super().__init__(**own)
296
+ plot._add_series(self)
297
+
298
+ def _parent(self) -> Settings:
299
+ return self.plot
300
+
301
+ def set(self, **settings: Any) -> Any:
302
+ super().set(**settings)
303
+ self._cache: dict[Any, Any] = {}
304
+ return self
305
+
306
+ def cached(self, key: Any, compute: Any) -> Any:
307
+ """``compute()`` once until a setting changes (derived data: bins, densities…)."""
308
+ cache = self.__dict__.setdefault("_cache", {})
309
+ if key not in cache:
310
+ cache[key] = compute()
311
+ return cache[key]
312
+
313
+ def __repr__(self) -> str:
314
+ name = self.get("name")
315
+ return f"{self.kind}({name!r})" if name else f"{self.kind}()"
316
+
317
+ # -- what the axes need --------------------------------------------------------
318
+
319
+ def has_axis(self, role: str) -> bool:
320
+ return True
321
+
322
+ def time_role(self, role: str) -> Any:
323
+ """``(True, tz)`` when the data on axis ``role`` were dates, else ``None``."""
324
+ for param, tz in getattr(self, "times", {}).items():
325
+ if self.column_role(param) == role:
326
+ return (True, tz)
327
+ return None
328
+
329
+ def column_role(self, param: str) -> str | None:
330
+ """The axis a data argument named ``param`` is drawn on (for axis titles taken from
331
+ column names)."""
332
+ if param in ("x", "y"):
333
+ return param
334
+ if param in ("values", "height", "categories"):
335
+ cat = getattr(self, "_cat_role", None)
336
+ if cat is not None:
337
+ role = cat()
338
+ return role if param == "categories" else ("y" if role == "x" else "x")
339
+ horizontal = "orientation" in self.SETTINGS and \
340
+ self.get("orientation") == "horizontal"
341
+ return "y" if horizontal else "x"
342
+ return None
343
+
344
+ def axis_title(self, role: str) -> Any:
345
+ """The column name drawn on axis ``role``, if the series was given a column."""
346
+ for param, name in getattr(self, "columns", {}).items():
347
+ if self.column_role(param) == role:
348
+ return name
349
+ return None
350
+
351
+ def categories(self, role: str) -> list[Any] | None:
352
+ """Categories on axis ``role``, or ``None`` for numeric data."""
353
+ return None
354
+
355
+ def values(self, role: str) -> Any:
356
+ """All data values on axis ``role`` (for quantile limits)."""
357
+ return None
358
+
359
+ def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
360
+ v = self.values(role)
361
+ if v is None:
362
+ return 0, math.nan, math.nan
363
+ return _backend.extent(v, scale.function)
364
+
365
+ def inverts(self, role: str) -> bool:
366
+ """Whether axis ``role`` is inverted by default for this series."""
367
+ return False
368
+
369
+ def zero_default(self, role: str) -> bool:
370
+ """Whether axis ``role`` includes 0 under ``include_zero="auto"`` (Vega-Lite)."""
371
+ return False
372
+
373
+ def zero_padding(self, role: str) -> bool:
374
+ """Whether automatic limits on axis ``role`` take no padding."""
375
+ return False
376
+
377
+ def color_sources(self) -> list[tuple[Any, Any]]:
378
+ """``(ColorMap, values)`` pairs the series maps: its color settings holding numbers
379
+ or a ``Density``, through ``color_map_obj()``."""
380
+ from ..values import Values, is_computed
381
+ out = []
382
+ for name in COLOR_SETTINGS:
383
+ if name in self.SETTINGS:
384
+ v = self.get(name)
385
+ if isinstance(v, Values) or is_computed(v):
386
+ out.append((self.color_map_obj(), self.point_values(v)))
387
+ return out
388
+
389
+ def size_sources(self) -> list[tuple[Any, Any]]:
390
+ out = []
391
+ for name in SIZE_SETTINGS:
392
+ m = self.size_map_obj(name)
393
+ if m is not None:
394
+ out.append((m, self.point_values(self.get(name))))
395
+ return out
396
+
397
+ def color_map_obj(self) -> Any:
398
+ """The ``ColorMap`` numbers in ``color`` go through: ``color_map`` when it is a
399
+ ``ColorMap`` (shared), else one made from it for this series."""
400
+ from ..maps import ColorMap
401
+ m = self.get("color_map") if "color_map" in self.SETTINGS else self.plot.get("color_map")
402
+ if isinstance(m, ColorMap):
403
+ return m
404
+ return self.cached(("color_map", repr(m)), lambda: ColorMap(m))
405
+
406
+ def size_map_obj(self, name: str = "size") -> Any:
407
+ """The ``SizeMap`` of a size setting holding values, or ``None`` (sizes in pt)."""
408
+ from ..maps import SizeMap
409
+ from ..values import is_computed
410
+ if name not in self.SETTINGS or "size_map" not in self.SETTINGS:
411
+ return None
412
+ v = self.get(name)
413
+ m = self.get("size_map")
414
+ if is_computed(v):
415
+ return m if m is not None else self.cached("size_map", SizeMap)
416
+ if m is not None and not isinstance(v, (int, float)):
417
+ return m
418
+ return None
419
+
420
+ def point_values(self, v: Any) -> Any:
421
+ """The numbers of a value setting: given ``Values``, sizes, or a computed
422
+ ``Density``."""
423
+ from ..values import Values, is_computed
424
+ if isinstance(v, Values):
425
+ return v.data
426
+ if is_computed(v):
427
+ return self.cached(("density", id(v)), lambda: v.compute(self))
428
+ return v
429
+
430
+ def palette_count(self) -> int:
431
+ """Palette colors the series takes when it has no color (one per value set of
432
+ multi-set series)."""
433
+ return 1
434
+
435
+ def auto_colors(self, ctx: Any) -> list[Any]:
436
+ """The palette colors given to the series (``palette_count()`` of them)."""
437
+ cols = getattr(self, "_auto_colors", None) or [self._auto_color]
438
+ return [ctx.color(c) for c in cols]
439
+
440
+ def has_own_color(self) -> bool:
441
+ return self.is_set("color")
442
+
443
+ def base_color(self, ctx: DrawContext) -> Any:
444
+ """The series color: its own, or the palette color it was given."""
445
+ c = self.get("color") if "color" in self.SETTINGS else None
446
+ if c is None:
447
+ c = self._auto_color
448
+ return ctx.color(c)
449
+
450
+ # -- drawing ---------------------------------------------------------------------
451
+
452
+ def draw(self, ctx: DrawContext) -> None:
453
+ raise NotImplementedError
454
+
455
+ def draw_overlay(self, ctx: DrawContext) -> None:
456
+ """Drawn after every series, outside the axes clip (value labels)."""
457
+
458
+ def legend_glyph(self, ctx: DrawContext, x: float, y: float, w: float, h: float) -> None:
459
+ """Draw the legend symbol in the box ``(x, y, w, h)`` (y: top)."""
460
+
461
+ def legend_colors(self, ctx: DrawContext) -> tuple[Any, Any]:
462
+ """``(fill, stroke)`` of simple legend symbols (``Legend.glyph``)."""
463
+ return self.base_color(ctx), None
464
+
465
+ def legend_labels(self) -> list[Any]:
466
+ """Legend entries of the object: its ``name``, or none."""
467
+ name = self.get("name")
468
+ return [] if name is None else [name]
469
+
470
+ def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float, h: float) -> None:
471
+ """The symbol of legend entry ``i`` (series with several entries override this)."""
472
+ self.legend_glyph(ctx, x, y, w, h)
473
+
474
+ def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
475
+ return self.legend_colors(ctx)
476
+
477
+ def occupy(self, ctx: DrawContext, occ: Any) -> None:
478
+ """Marks what the series draws on ``occ`` (an :class:`~plot_io.occupancy.Occupancy`),
479
+ for legend placement; by default its legend points."""
480
+ pts = self.legend_points()
481
+ if pts is not None:
482
+ from ..occupancy import thin
483
+ xs, ys = thin(*(_backend.doubles(v) for v in pts))
484
+ size = self.get("size") if "size" in self.SETTINGS else None
485
+ radius = size / 2 if isinstance(size, (int, float)) else 0.0
486
+ occ.points(ctx.xmap.many(xs), ctx.ymap.many(ys), radius)
487
+
488
+ def occupy_all(self, ctx: DrawContext, occ: Any) -> None:
489
+ """Marks the whole axes (series that fill them: heatmaps, contours)."""
490
+ occ.rects([ctx.rect[0]], [ctx.rect[1]], [ctx.rect[2]], [ctx.rect[3]])
491
+
492
+ def legend_points(self) -> tuple[Any, Any] | None:
493
+ """``(xs, ys)`` data points for the legend's ``auto`` placement."""
494
+ return None
495
+
496
+ def simplify_arg(self, lines: bool = False) -> Any:
497
+ """pdf_io's ``simplify`` for this series' calls (``lines``: lines and areas)."""
498
+ return _backend.simplify_arg(self.get("simplify"), lines)