plot-io 0.12.0__py3-none-win_arm64.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-arm64.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/layers/base.py ADDED
@@ -0,0 +1,630 @@
1
+ """The model of a plot type's call: data plus settings (:class:`Series`).
2
+
3
+ A series is what ``p.scatter(…)`` or ``plt.Box(…)`` returns. It holds the data (its
4
+ :class:`~plot_io.columns.Columns`, ``cols``) and the settings, and one method,
5
+ :meth:`Series.layers`: the preset that turns it into a :class:`~plot_io.layers.core.Call`
6
+ (its layers, legend entries and what the axes and color maps need) in each render.
7
+ :class:`SeriesMeta` resolves ``data=`` columns, reads the data arguments into the
8
+ ``Columns`` and splits ``group=`` calls before a series is made.
9
+ """
10
+
11
+ import math
12
+ from typing import Any
13
+
14
+ from .. import groups
15
+ from ..color.values import color as _color # noqa: F401 (re-exported for presets)
16
+ from ..color.values import VARIANT_DOC, check_variant
17
+ from ..levels import level_order
18
+ from .stats import check_estimate, check_interval
19
+ from ..limits import check_limits
20
+ from ..scales import NUMERIC_SCALES
21
+ from ..settings import (Setting, Settings, dpi_or_bool, number, one_of, route, simplify_value,
22
+ table, text)
23
+ from ..units import NeedsScale
24
+ from ..validators import length, typed
25
+ from ..values import lazy
26
+
27
+ __all__ = ["Series", "SeriesMeta", "column", "COMMON", "common", "upward_message",
28
+ "slot_width", "slot_parts"]
29
+
30
+ _check_color_map = lazy("color.maps", "check_series_map")
31
+ _check_size_map = lazy("values", "check_size_map")
32
+ _check_color_axis = lazy("mapped", "check_color_axis")
33
+ _check_size_axis = lazy("mapped", "check_size_axis")
34
+
35
+
36
+ @typed("bool | str | Mapping[str, Any]")
37
+ def colorbar_value(v: Any) -> Any:
38
+ """A ``colorbar=`` value: ``True``, ``False``, a place, or a dict of colorbar
39
+ settings."""
40
+ from ..guides import where_of
41
+ if isinstance(v, bool):
42
+ return v
43
+ if isinstance(v, dict):
44
+ return dict(v)
45
+ if isinstance(v, str) and "visible" not in where_of(v):
46
+ return v
47
+ raise ValueError(f"expected True, False, a place or a dict of colorbar settings, "
48
+ f"got {v!r}")
49
+
50
+
51
+ @typed("Literal['key', 'legend', False] | Sequence[SupportsFloat]")
52
+ def _size_key(v: Any) -> Any:
53
+ if v is False:
54
+ return v
55
+ if isinstance(v, str) and v.strip().lower() in ("key", "legend"):
56
+ return v.strip().lower()
57
+ if isinstance(v, (list, tuple)) and v:
58
+ return [float(x) for x in v]
59
+ raise ValueError(f"expected 'key', 'legend', False or a list of values, got {v!r}")
60
+
61
+
62
+ COMMON = dict(
63
+ name=lambda: Setting(text, doc="Legend entry; unset: not in the legend."),
64
+ opacity=lambda: Setting(number(0, 1), inherited=True,
65
+ doc="Opacity, multiplying the colors' own."),
66
+ z=lambda: Setting(number(), doc="Drawing order (higher on top); default: call order."),
67
+ rasterize=lambda: Setting(dpi_or_bool, inherited=True,
68
+ doc="`True` or a dpi: drawn as an image inside the PDF."),
69
+ simplify=lambda: Setting(simplify_value, inherited=True,
70
+ doc="`\"auto\"`, `True`, a dpi, `False`, or a `Simplify` (or "
71
+ "a dict of its fields): vec_io's `simplify`."),
72
+ text_color=lambda: Setting(_color, inherited=True, doc="Text color."),
73
+ plain_color=lambda: Setting(_color, inherited=True,
74
+ doc="Color when the series needs no hue (no levels, one "
75
+ "color, no `name`), the plot's turn kept; unset: the "
76
+ "plot's next color."),
77
+ line_width=lambda: Setting(length(lo=0), inherited=True, doc="Line width."),
78
+ color_variant=lambda: Setting(check_variant, inherited=True, doc=VARIANT_DOC),
79
+ color_map=lambda: Setting(_check_color_map,
80
+ doc="The colors of numbers given as colors, and of the series' "
81
+ "levels, sets and group members: a `ColorMap`, a map name "
82
+ "or colors (alias `cmap`), a dict level → color, `\"auto\"` "
83
+ "(numbers: `sequential_map`, or `diverging_map` about a "
84
+ "`center`; levels: the plot's `qualitative_map` colors) "
85
+ "or a kind (`\"sequential\"`…: that kind's map)."),
86
+ center=lambda: Setting(number(),
87
+ doc="Numbers given as colors: the value at the color map's "
88
+ "middle (`color_map=\"auto\"`: `diverging_map`); the "
89
+ "limits symmetric about it unless given."),
90
+ color_limits=lambda: Setting(check_limits,
91
+ doc="Limits of the series' color axis, as an axis' `limits`: "
92
+ "`\"-2/2\"`, `\"0/max\"`, `\"q:0.01/q:0.99\"`, "
93
+ "`\"-m/m\"` (symmetric about the `center`, or 0, `m` "
94
+ "the farthest value); unset: `\"auto\"`, the values' "
95
+ "range."),
96
+ color_scale=lambda: Setting(one_of(*NUMERIC_SCALES),
97
+ doc="Scale of the series' color axis (`log`, `sqrt`, …); "
98
+ "unset: `linear`."),
99
+ color_axis=lambda: Setting(_check_color_axis, expr=False,
100
+ doc="A shared `ColorAxis` (`figure.color_axis(…)`) in place of "
101
+ "the series' own: it holds the map, limits and scale."),
102
+ colorbar=lambda: Setting(colorbar_value, expr=False,
103
+ doc="A colorbar of the series' color axis attached to the plot "
104
+ "(once): `True`, a place (a side or a place inside the "
105
+ "axes) or a dict of colorbar settings; `False`: none."),
106
+ size_key=lambda: Setting(_size_key,
107
+ doc="With mapped sizes: a key attached to the plot, as a "
108
+ "colorbar's (`\"key\"`), sample rows in the legend "
109
+ "(`\"legend\"`), none (`False`), or the values to show (a "
110
+ "list: a key with a marker at each)."),
111
+ size_map=lambda: Setting(_check_size_map,
112
+ doc="The sizes numbers in `size` are drawn with: a `SizeMap` "
113
+ "(alias `smap`); without it (or `size_axis`), numbers are "
114
+ "sizes in px."),
115
+ size_limits=lambda: Setting(check_limits,
116
+ doc="Limits of the series' size axis, as an axis' `limits`; "
117
+ "unset: `\"auto\"`."),
118
+ size_scale=lambda: Setting(one_of(*NUMERIC_SCALES),
119
+ doc="Scale of the series' size axis; unset: `linear`."),
120
+ size_axis=lambda: Setting(_check_size_axis, expr=False,
121
+ doc="A shared `SizeAxis` (`figure.size_axis(…)`) in place of "
122
+ "the series' own: it holds the sizes, limits and scale."),
123
+ markers=lambda: Setting(level_order,
124
+ doc="Markers of marker levels: a list taken in turn, or a dict "
125
+ "level → marker; default: circle, square, triangle_up…"),
126
+ line_styles=lambda: Setting(level_order,
127
+ doc="Line styles of line_style levels: a list taken in turn, "
128
+ "or a dict level → style; default: solid, dashed, dotted, "
129
+ "dashdot."),
130
+ level_order=lambda: Setting(level_order,
131
+ doc="Levels first, in order (kept when absent): a list, or a "
132
+ "dict of lists by column."),
133
+ estimate=lambda: Setting(check_estimate,
134
+ doc="Replicates summarized per position (and set, and level): "
135
+ "`\"mean\"`, `\"median\"` or a function of an array; "
136
+ "default none (each value drawn)."),
137
+ interval=lambda: Setting(check_interval,
138
+ doc="With `estimate`, the interval drawn as error bars: "
139
+ "`\"ci\"` (95 % t interval of the mean; `(\"ci\", 0.9)`), "
140
+ "`\"se\"`, `\"sd\"`, `\"pi\"` (95 % percentile interval "
141
+ "of the values; `(\"pi\", 0.5)`); default none."),
142
+ level_missing=lambda: Setting(one_of("keep", "drop"),
143
+ doc="Missing values of a level column: `\"keep\"` (a "
144
+ "level `\"nan\"`) or `\"drop\"` (their rows left "
145
+ "out)."),
146
+ )
147
+
148
+ #: Settings a name of :func:`common` brings along: the color of a series without a legend
149
+ #: entry, the color variant (every series has ``z``), the series' color and size axes, the
150
+ #: settings of level axes.
151
+ BUNDLES = {"name": ("plain_color",), "z": ("color_variant",),
152
+ "color_map": ("color_limits", "color_scale", "center", "color_axis", "colorbar"),
153
+ "size_map": ("size_limits", "size_scale", "size_axis"),
154
+ "level_order": ("level_missing", "color_map"),
155
+ "estimate": ("interval",)}
156
+
157
+
158
+ #: Data arguments whose strings name columns when ``data`` is given (others than the
159
+ #: positional ones).
160
+ DATA_KEYS = ("color", "edge_color", "marker_color", "marker_edge_color", "size", "opacity",
161
+ "group", "draw_order", "x_error", "y_error", "weights", "positions", "x", "y",
162
+ "names", "marker", "line_style", "labels")
163
+ #: Of those, the ones whose strings must be columns.
164
+ MUST_BE_COLUMNS = ("group", "weights", "positions")
165
+ #: Per-point keyword arguments split among the members of a group.
166
+ SPLIT_KEYS = ("color", "edge_color", "marker_color", "marker_edge_color", "size", "opacity",
167
+ "marker", "x_error", "y_error", "draw_order", "weights", "x", "y", "names",
168
+ "labels")
169
+
170
+
171
+ def call_signature(fn: Any, drop: tuple[str, ...] = (), first: tuple[str, ...] = ()) -> Any:
172
+ """The signature of ``fn`` without annotations and the parameters in ``drop``, after
173
+ positional parameters named ``first``."""
174
+ import inspect
175
+ sig = inspect.signature(fn)
176
+ params = [inspect.Parameter(n, inspect.Parameter.POSITIONAL_OR_KEYWORD) for n in first]
177
+ params += [p.replace(annotation=p.empty) for n, p in sig.parameters.items()
178
+ if n not in drop]
179
+ return inspect.Signature(params)
180
+
181
+
182
+ def _positional(cls: type) -> list[str]:
183
+ import inspect
184
+ names = cls.__dict__.get("_POSITIONAL")
185
+ if names is None:
186
+ sig = inspect.signature(getattr(cls, "__init__"))
187
+ names = [n for n, p in sig.parameters.items()
188
+ if n not in ("self", "plot") and p.kind in (p.POSITIONAL_ONLY,
189
+ p.POSITIONAL_OR_KEYWORD)]
190
+ setattr(cls, "_POSITIONAL", names)
191
+ return names
192
+
193
+
194
+ _REQUIRED = object()
195
+
196
+
197
+ def _defaults(cls: type) -> dict[str, Any]:
198
+ """The defaults of ``cls.__init__``'s parameters (:data:`_REQUIRED`: none)."""
199
+ import inspect
200
+ out = cls.__dict__.get("_DEFAULTS")
201
+ if out is None:
202
+ sig = inspect.signature(getattr(cls, "__init__"))
203
+ out = {n: (_REQUIRED if p.default is p.empty else p.default)
204
+ for n, p in sig.parameters.items()}
205
+ setattr(cls, "_DEFAULTS", out)
206
+ return out
207
+
208
+
209
+ def _length(v: Any) -> int | None:
210
+ if v is None or isinstance(v, (str, bytes, dict, int, float)):
211
+ return None
212
+ try:
213
+ return len(v)
214
+ except TypeError:
215
+ return None
216
+
217
+
218
+ class SeriesMeta(type):
219
+ """Resolves ``data`` columns, reads the data arguments (the type's ``DATA``) into a
220
+ :class:`~plot_io.columns.Columns` and splits ``group`` calls before a series is made."""
221
+
222
+ @property
223
+ def __signature__(cls) -> Any:
224
+ """The parameters of ``__init__`` (without ``self``; ``plot`` keyword-only, as
225
+ :meth:`__call__` takes it; ``y=None`` after the first when a ``y`` makes another
226
+ type), for :func:`inspect.signature`."""
227
+ import inspect
228
+ sig = call_signature(getattr(cls, "__init__"), drop=("self",))
229
+ params = [p.replace(kind=p.KEYWORD_ONLY) if n == "plot" else p
230
+ for n, p in sig.parameters.items()]
231
+ if getattr(cls, "with_y", None) is not None and "y" not in sig.parameters:
232
+ params.insert(1, inspect.Parameter("y", inspect.Parameter.POSITIONAL_OR_KEYWORD,
233
+ default=None))
234
+ return sig.replace(parameters=sorted(params, key=lambda p: p.kind))
235
+
236
+ def __call__(cls, *args: Any, **kw: Any) -> Any:
237
+ from .. import tables
238
+ from ..settings import canonical_kwargs
239
+ with_y = getattr(cls, "with_y", None)
240
+ if with_y is not None and (len(args) == 2 or "y" in kw):
241
+ return with_y()(*args, **kw)
242
+ names = _positional(cls)
243
+ kw = canonical_kwargs(kw, getattr(cls, "SETTINGS", {}), cls.__name__)
244
+ if len(args) > len(names):
245
+ raise TypeError(f"{cls.__name__}() takes {len(names)} positional arguments")
246
+ pos = dict(zip(names, args))
247
+ for n in names:
248
+ if n in kw:
249
+ if n in pos:
250
+ raise TypeError(f"{cls.__name__}() got two values for {n!r}")
251
+ pos[n] = kw.pop(n)
252
+ plot = kw.get("plot")
253
+ data = kw.pop("data", None)
254
+ if data is None and plot is not None:
255
+ data = plot.resolve("data")
256
+ columns: dict[str, Any] = {}
257
+ orders: dict[str, Any] = {}
258
+ if data is not None:
259
+ for n, v in list(pos.items()):
260
+ if isinstance(v, str):
261
+ pos[n], orders[n] = tables.column(data, v)
262
+ columns[n] = v
263
+ for n in DATA_KEYS:
264
+ v = kw.get(n)
265
+ if isinstance(v, str) and n not in pos:
266
+ if tables.has_column(data, v):
267
+ if n in ("color", "edge_color", "marker_color", "marker_edge_color"):
268
+ from ..color.values import color
269
+ try:
270
+ color(v)
271
+ tables.color_name_warning(v)
272
+ except ValueError:
273
+ pass
274
+ kw[n], orders[n] = tables.column(data, v)
275
+ columns[n] = v
276
+ elif n in MUST_BE_COLUMNS:
277
+ raise ValueError(f"no column {v!r} in data")
278
+ from ..columns import Columns, Keys
279
+ kinds: dict[str, str] = getattr(cls, "DATA", {})
280
+ if "categories" not in kinds.values():
281
+ for store in (pos, kw):
282
+ for n in ("x", "y"):
283
+ if orders.get(n) is not None and n in store and kinds.get(n) != "cells":
284
+ store[n] = Keys(store[n], orders[n])
285
+ from .. import times as _times
286
+ names = list(kinds)
287
+ if len(names) > 1 and names[0] == "x" and pos.get(names[1]) is None \
288
+ and kinds[names[1]] in ("numbers", "sets"):
289
+ index: Any = getattr(pos.get("x"), "index", None) # a pandas date index
290
+ if getattr(getattr(index, "dtype", None), "kind", None) == "M":
291
+ pos[names[1]] = pos["x"]
292
+ pos["x"] = index if getattr(index, "tz", None) is None else list(index)
293
+ time_params: dict[str, Any] = {}
294
+ for store in (pos, kw):
295
+ for n in ("x", "y", "positions"):
296
+ v = store.get(n)
297
+ if v is not None and _times.is_time_like(v):
298
+ store[n], time_params[n] = _times.to_seconds(v)
299
+ kw = cls._at_call(pos, kw) # type: ignore[attr-defined]
300
+ defaults = _defaults(cls)
301
+ if any(n not in pos and defaults.get(n, 0) is _REQUIRED for n in kinds):
302
+ from ..settings import SAME_IDEA
303
+ for k in kw:
304
+ if SAME_IDEA.get(k) in kinds and SAME_IDEA[k] not in pos:
305
+ raise TypeError(f"{cls.__name__}: unknown argument {k!r} (did you mean "
306
+ f"{SAME_IDEA[k]!r}?)")
307
+ return super().__call__(**pos, **kw) # the missing argument's error
308
+ kind = getattr(cls, "LEVELS", None)
309
+ if kind == "grid" and "group" in kw:
310
+ raise ValueError(f"{cls.__name__}: group= splits points, not grids; use plt.facet")
311
+ from .. import levels
312
+ found = levels.detect(cls, kw, columns, orders)
313
+ given = {n: pos[n] if n in pos else defaults.get(n) for n in kinds}
314
+ split = "group" in kw and getattr(cls, "SPLITS_GROUPS", True) and not found
315
+ cols = Columns.build(kinds, given, kw, columns, orders, time_params,
316
+ getattr(cls, "ITEMS", "sets"),
317
+ read=not split and not (found and kind == "split"))
318
+ if split:
319
+ return cls._grouped(cols, pos, kw)
320
+ for k in ("group_order", "group_missing", "group_colors"):
321
+ if k in kw:
322
+ raise ValueError(f"{k} applies with group=")
323
+ if found:
324
+ return levels.make(cls, cols, pos, kw, found)
325
+ return cls._make(cols, pos, kw)
326
+
327
+ def _make(cls, cols: Any, pos: dict[str, Any], kw: dict[str, Any]) -> Any:
328
+ """A series of ``cls`` reading ``cols`` (``pos``: its data arguments as given)."""
329
+ s: Any = object.__new__(cls) # type: ignore[arg-type]
330
+ s.cols = cols
331
+ s.__init__(**pos, **kw)
332
+ return s
333
+
334
+ def _grouped(cls, cols: Any, pos: dict[str, Any], kw: dict[str, Any]) -> Any:
335
+ from .. import tables
336
+ labels = kw.pop("group")
337
+ given = kw.pop("group_order", None)
338
+ missing = kw.pop("group_missing", "keep")
339
+ if missing not in ("keep", "drop"):
340
+ raise ValueError("group_missing is 'keep' or 'drop'")
341
+ colors = kw.pop("group_colors", None) or {}
342
+ from ..mapped import group_axes
343
+ group_axes(cls, kw)
344
+ bar = kw.pop("colorbar", None) # one colorbar of the shared color axis
345
+ n = _length(labels)
346
+ if n is None:
347
+ raise ValueError("group needs one label per point")
348
+ if hasattr(labels, "tolist"):
349
+ labels = labels.tolist()
350
+ keys, rows = tables.group_keys(labels, cols.orders.get("group"), given, missing)
351
+ auto_marker = kw.get("marker") == "auto"
352
+ auto_style = kw.get("line_style") == "auto"
353
+ title = cols.titles.get("group")
354
+ members: list[Any] = []
355
+ token = object() # the members drawn in data order
356
+ for k, (key, idx) in enumerate(zip(keys, rows)):
357
+ ck = cols.take(idx, n)
358
+ kk = {}
359
+ for nm, v in kw.items():
360
+ if nm in SPLIT_KEYS and _length(v) == n and not (
361
+ isinstance(v, tuple) and len(v) in (3, 4)):
362
+ v = tables.take(v, idx)
363
+ kk[nm] = v
364
+ if auto_marker:
365
+ kk["marker"] = tables.MARKER_CYCLE[k % len(tables.MARKER_CYCLE)]
366
+ if auto_style:
367
+ kk["line_style"] = tables.LINE_STYLE_CYCLE[k % len(tables.LINE_STYLE_CYCLE)]
368
+ if key in colors:
369
+ kk["color"] = colors[key]
370
+ kk.setdefault("name", str(key))
371
+ if k > 0 and kk.get("plot") is None and members:
372
+ kk["plot"] = members[0].plot
373
+ pk = {nm: ck.args[nm] if nm in ck.args else (
374
+ tables.take(v, idx) if _length(v) == n else v) for nm, v in pos.items()}
375
+ m = cls._make(ck, pk, kk)
376
+ m.__dict__["_member"] = (k, len(keys)) # its color of an own color_map
377
+ m.__dict__["_order_rows"] = (token, idx)
378
+ members.append(m)
379
+ if bar is not None and members:
380
+ members[-1].set(colorbar=bar)
381
+ group = tables.SeriesGroup(members, keys, title)
382
+ if members and title is not None:
383
+ members[0].plot._legend_title = title
384
+ return group
385
+
386
+
387
+ def common(*names: str, docs: dict[str, str] | None = None) -> dict[str, Setting]:
388
+ """The ``COMMON`` settings ``names``, and members of the :class:`~plot_io.groups.Font`
389
+ group (``font_family`` brings the whole face along; ``color_map`` and ``size_map``
390
+ bring the settings of the series' color and size axes, :data:`BUNDLES`); ``docs``:
391
+ the type's own doc of some of them."""
392
+ font = [n for n in groups.Font.MEMBERS if n in names
393
+ or (n in groups.Font.FACE and "font_family" in names)]
394
+ names = tuple(m for n in names for m in (n, *BUNDLES.get(n, ())))
395
+ out = {n: COMMON[n]() for n in names if n not in groups.Font.MEMBERS}
396
+ for n, doc in (docs or {}).items():
397
+ out[n].doc = doc
398
+ return {**out, **groups.Font.embed(only=font)}
399
+
400
+
401
+ def upward_message(kind: str, levels: list[tuple[str, str, dict[str, Any]]]) -> str:
402
+ """The error of keywords that belong to levels above an object that already exist:
403
+ keywords set the levels above only when the call creates them. ``levels``:
404
+ ``(level, how to reach it, keywords)``."""
405
+ parts = []
406
+ for level, target, kw in levels:
407
+ if kw:
408
+ names = ", ".join(f"{k}=" for k in kw)
409
+ args = ", ".join(f"{k}=…" for k in kw)
410
+ parts.append(f"{names} of the existing {level}: use {target}.set({args})")
411
+ return f"{kind}: " + "; ".join(parts)
412
+
413
+
414
+ class Series(Settings, metaclass=SeriesMeta):
415
+ """Base of every plot type's model. ``plot=None`` creates a plot (and its figure).
416
+
417
+ A subclass declares its ``DATA`` and ``SETTINGS`` and implements :meth:`layers`."""
418
+
419
+ SETTINGS: dict[str, Setting] = table(**common("name", "opacity", "z", "rasterize",
420
+ "simplify"))
421
+
422
+ #: The data arguments and their slot kinds (:data:`plot_io.columns.KINDS`), read into
423
+ #: :attr:`cols` once per call.
424
+ DATA: dict[str, str] = {}
425
+ #: The data of the call (:class:`~plot_io.columns.Columns`).
426
+ cols: Any
427
+ #: Whether ``group=`` makes one series per group (else the series takes it).
428
+ SPLITS_GROUPS = True
429
+
430
+ def __init__(self, *, plot: Any = None, **settings: Any) -> None:
431
+ from ..figure import Figure
432
+ from ..plot import Plot
433
+
434
+ own, plot_kw, fig_kw = route(settings, [type(self), Plot, Figure],
435
+ extra={1: Plot.CALL_EXTRA})
436
+ if plot is None:
437
+ plot = Plot(**plot_kw, **fig_kw)
438
+ elif plot_kw or fig_kw:
439
+ raise ValueError(upward_message(self.kind, [("plot", "plot", plot_kw),
440
+ ("figure", "plot.figure", fig_kw)]))
441
+ self.plot = plot
442
+ super().__init__(**own)
443
+ if "_of" not in self.__dict__: # a level member is drawn by its series
444
+ plot._add_series(self)
445
+ self._ready = True
446
+ self._attach_bar()
447
+
448
+ def _parent(self) -> Settings:
449
+ return self.plot
450
+
451
+ @property
452
+ def columns(self) -> dict[str, Any]:
453
+ """The ``data=`` column name of each data argument."""
454
+ return self.cols.titles
455
+
456
+ @classmethod
457
+ def _at_call(cls, data: dict[str, Any], kw: dict[str, Any]) -> dict[str, Any]:
458
+ """The keywords of a call given its data arguments (``data=`` columns read): a type
459
+ deriving settings from its data at the call overrides it."""
460
+ return kw
461
+
462
+ #: How the type draws levels in its aesthetics (:mod:`plot_io.levels`): ``"items"``,
463
+ #: ``"split"``; ``"grid"`` refuses them and ``group=``; ``None``: not read as levels.
464
+ LEVELS: str | None = None
465
+ #: The level axes and calls of a series with levels (:class:`plot_io.levels.Levels`).
466
+ _lv: Any = None
467
+
468
+ def set(self, **settings: Any) -> Any:
469
+ if self._lv is not None and "name" in settings: # with levels: the legend's title
470
+ self.plot._legend_title = settings.pop("name")
471
+ super().set(**settings)
472
+ self._cache: dict[Any, Any] = {}
473
+ if self._lv is not None:
474
+ for part, _, _ in self._lv.parts:
475
+ if part is not self:
476
+ part.set(**settings)
477
+ for name in ("color_axis", "size_axis"):
478
+ axis = self._values.get(name)
479
+ if axis is not None and axis._parent() is None:
480
+ self.plot.figure.adopt_axis(axis) # a shared axis made on its own
481
+ if "colorbar" in settings and self._ready:
482
+ self._attach_bar()
483
+ return self
484
+
485
+ #: Whether the series is made (its ``__init__`` done).
486
+ _ready = False
487
+ #: The colorbar ``colorbar=`` attached, once.
488
+ _bar: Any = None
489
+
490
+ def _attach_bar(self) -> None:
491
+ """``colorbar=``: a colorbar of :meth:`_bar_source` attached to the plot, once (and
492
+ not twice for one source)."""
493
+ v = self.resolve("colorbar") if "colorbar" in self.SETTINGS else False
494
+ if not v or self._bar is not None or "_of" in self.__dict__:
495
+ return
496
+ source = self._bar_source()
497
+ if source is None or any(cb.source is source for cb in self.plot.colorbars):
498
+ return
499
+ if isinstance(v, dict):
500
+ self._bar = self.plot.add_colorbar(source, **v)
501
+ else:
502
+ self._bar = self.plot.add_colorbar(source, None if v is True else v)
503
+
504
+ def _bar_source(self) -> Any:
505
+ """What ``colorbar=`` shows: the series when it maps numbers in ``color`` (or in a
506
+ marker or edge color); else ``None`` (a ``colorbar=`` of the series' own raises)."""
507
+ from ..values import Values, is_computed
508
+ for name in ("color", "marker_color", "edge_color", "marker_edge_color"):
509
+ v = self.resolve(name) if name in self.SETTINGS else None
510
+ if isinstance(v, Values) or is_computed(v):
511
+ return self
512
+ if self.is_set("colorbar"):
513
+ raise ValueError(f"{self.kind}: colorbar= needs numbers in color (or in a "
514
+ f"marker or edge color)")
515
+ return None
516
+
517
+ def _children(self) -> list[Settings]:
518
+ return [a for a in (self.__dict__.get("_color_axis"),
519
+ self.__dict__.get("_size_axis")) if a is not None]
520
+
521
+ @property
522
+ def color_axis(self) -> Any:
523
+ """The color axis numbers in ``color`` go through: the shared one given as
524
+ ``color_axis=``, else the series' own (its scale and limits are the series'
525
+ ``color_scale`` and ``color_limits``); its guide is a colorbar."""
526
+ from ..mapped import color_axis_of
527
+ if "color_map" not in self.SETTINGS:
528
+ raise AttributeError(f"{self.kind} maps no colors")
529
+ return color_axis_of(self)
530
+
531
+ @property
532
+ def size_axis(self) -> Any:
533
+ """The size axis numbers in ``size`` go through (as :attr:`color_axis`); its guide
534
+ is a size key."""
535
+ from ..mapped import SizeAxis, own_axis, shared_axis
536
+ if "size_map" not in self.SETTINGS:
537
+ raise AttributeError(f"{self.kind} maps no sizes")
538
+ shared = shared_axis(self, "size")
539
+ return shared if shared is not None else own_axis(self, SizeAxis)
540
+
541
+ def cached(self, key: Any, compute: Any, reads: tuple[str, ...] = (),
542
+ kind: Any = None) -> Any:
543
+ """``compute()`` once per ``key`` and resolved values of the settings it ``reads``
544
+ (from the series, its parents or the style), until a setting of the series changes
545
+ (derived data: bins, densities…). One result is kept per ``kind`` (default: the
546
+ key): a new key of a kind replaces the previous."""
547
+ cache = self.__dict__.setdefault("_cache", {})
548
+ full = (key, tuple(_token(self.resolve(n)) for n in reads if n in self.SETTINGS))
549
+ slot = key if kind is None else kind
550
+ hit = cache.get(slot)
551
+ if hit is None or hit[0] != full:
552
+ hit = cache[slot] = (full, compute())
553
+ return hit[1]
554
+
555
+ def __repr__(self) -> str:
556
+ name = self.get("name")
557
+ return f"{self.kind}({name!r})" if name else f"{self.kind}()"
558
+
559
+ #: the type as a declaration (:class:`~plot_io.layers.grammar.Spec`), run by
560
+ #: :meth:`layers`; ``None``: the type implements :meth:`layers`
561
+ SPEC: Any = None
562
+
563
+ def layers(self) -> Any:
564
+ """The preset: this call's :class:`~plot_io.layers.core.Call` (layers, legend
565
+ entries, what the axes and color maps need), built in each render (by the
566
+ type's ``SPEC`` when it has one)."""
567
+ if self.SPEC is not None:
568
+ from .grammar import build
569
+ return build(self)
570
+ raise NotImplementedError
571
+
572
+
573
+ def column(name: str, doc: str) -> Any:
574
+ """A read-only property: the series' column ``name`` (``cols[name]``)."""
575
+ return property(lambda self: self.cols[name], doc=doc)
576
+
577
+
578
+ def _token(v: Any) -> Any:
579
+ """A setting value as a hashable part of a cache key (containers by content, other
580
+ unhashable values by identity)."""
581
+ if isinstance(v, (list, tuple)):
582
+ return (type(v).__name__, tuple(_token(x) for x in v))
583
+ if isinstance(v, dict):
584
+ return ("dict", tuple((k, _token(x)) for k, x in v.items()))
585
+ try:
586
+ hash(v)
587
+ except TypeError:
588
+ return ("id", id(v))
589
+ return v
590
+
591
+
592
+ def slot_width(series: Any, positions: Any, time: bool, scale: float | None = None,
593
+ name: str = "width") -> float:
594
+ """``width`` (or ``name``) of a categorical series in data units: `%` of the slot (one
595
+ category, or the smallest gap between ``positions``), lengths through ``scale`` (px per
596
+ data unit; 0 while it is unknown)."""
597
+ unit = 1.0
598
+ if positions is not None:
599
+ ps = sorted({float(p) for p in positions})
600
+ gaps = [b - a for a, b in zip(ps, ps[1:]) if b > a]
601
+ unit = min(gaps) if gaps else 1.0
602
+ else:
603
+ time = False
604
+ try:
605
+ return series.resolve(name, of=unit, scale=abs(scale) if scale else None,
606
+ time=time, unit="data")
607
+ except NeedsScale:
608
+ return 0.0
609
+
610
+
611
+ def slot_parts(series: Any, positions: Any, time: bool, axis_scale: Any,
612
+ scale: float | None = None, name: str = "width") -> tuple[float, float]:
613
+ """``width`` (or ``name``) of a series at numeric ``positions`` on an axis with a scale
614
+ function, in two parts: ``%`` of the slot (the smallest gap between the positions
615
+ through the function) and lengths in the scale's space (``scale``: px per unit of it;
616
+ 0 while it is unknown), and lengths in data units (``data``, ``d``), which stay data
617
+ units."""
618
+ fs = sorted({v for v in (axis_scale.f(float(p)) for p in positions) if math.isfinite(v)})
619
+ gaps = [b - a for a, b in zip(fs, fs[1:]) if b > a]
620
+ unit = min(gaps) if gaps else 1.0
621
+ try:
622
+ data = series.resolve(name, of=0.0, scale=math.inf, time=time, unit="data")
623
+ except ValueError: # no data part (a % or px width alone)
624
+ data = 0.0
625
+ try:
626
+ total = series.resolve(name, of=unit, scale=abs(scale) if scale else None,
627
+ time=time, unit="data")
628
+ except NeedsScale:
629
+ return 0.0, data
630
+ return max(total - data, 0.0), data