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
@@ -0,0 +1,586 @@
1
+ """The grammar: a plot type as a declaration (:class:`Spec`) run by one engine
2
+ (:func:`build`).
3
+
4
+ A spec names its tables (built from the call's columns, or by a stat), its layers
5
+ (:class:`Lay`: a geom, a paint role, the table it draws, when it is drawn, whether the
6
+ legend symbol shows it), its legend entries, what it takes of the plot's colors and what the
7
+ axes need. The engine runs it: columns → tables (a stat's output, cached) → layers
8
+ (orientation is one swap: ``flip``) → legend entries (symbols as data) → colors taken →
9
+ axes. A layer's ``stat`` runs once per call (:func:`~plot_io.layers.stats.run`; its
10
+ output in ``build.stats`` for the table functions), and its ``position`` is asked of the
11
+ tables it draws that hold ``heights`` (:func:`ask`): stacked, dodged or from 0; the
12
+ positions run over the plot's calls (:mod:`~plot_io.layers.positions`). :func:`top` reads a built call back: the highest value
13
+ it draws over a span of the other axis (brackets).
14
+ """
15
+
16
+ from typing import Any, Callable
17
+
18
+ from .core import Call, Entry, Layer, Table
19
+ from .geoms import GEOMS
20
+ from .paint import Paint
21
+
22
+ __all__ = ["Spec", "Lay", "Build", "build", "ask", "entry", "holds", "flipped", "axis_of", "top"]
23
+
24
+
25
+ def flipped(series: Any) -> bool:
26
+ """Whether a series is drawn horizontally (``orientation="horizontal"``: its data's x
27
+ on the plot's y axis)."""
28
+ return "orientation" in series.SETTINGS and series.resolve("orientation") == "horizontal"
29
+
30
+
31
+ def axis_of(series: Any, data_role: str = "x") -> Any:
32
+ """The plot axis a data role of ``series`` is drawn on."""
33
+ plot = series.plot
34
+ return plot.y_axis if flipped(series) == (data_role == "x") else plot.x_axis
35
+
36
+
37
+ def holds(series: Any, cond: Any) -> bool:
38
+ """Whether condition ``cond`` holds for ``series``: ``None`` or ``True`` (always),
39
+ ``False`` (never), a setting name
40
+ (set to a true value), ``"name()"`` (a method's result), ``(name, value)`` or
41
+ ``(name, (values…))`` (a setting's value), or a function of the series."""
42
+ if cond is None or isinstance(cond, bool):
43
+ return cond is not False
44
+ if callable(cond):
45
+ return bool(cond(series))
46
+ if isinstance(cond, tuple):
47
+ name, want = cond
48
+ v = series.resolve(name)
49
+ return v in want if isinstance(want, tuple) else v == want
50
+ if cond.endswith("()"):
51
+ return bool(getattr(series, cond[:-2])())
52
+ return bool(series.resolve(cond))
53
+
54
+
55
+ class Lay:
56
+ """One layer of a spec: ``geom`` (a name of :data:`~plot_io.layers.geoms.GEOMS`, or
57
+ ``"errors"``: the error bars of ``x_error``/``y_error``) drawing ``table`` with
58
+ ``paint = (role, options)`` (:class:`~plot_io.layers.paint.Paint`; an option ``"@name"``
59
+ is the paint of the layer ``name``, ``"#name"`` the table ``name``), ``when`` it holds (:func:`holds`). ``key``: the
60
+ legend symbol shows it (``"first"``: its first table's layer only); ``drawn``: it is
61
+ drawn on the axes (not: a symbol part only); ``per_set``: the paint takes the index of
62
+ the table among the table list's (``index``), and consecutive per-set layers of one
63
+ table list are drawn set by set; ``turn``: drawn along the value axis (the geom's x on
64
+ the plot's y, violins). ``stat``: the registered stat whose output the table
65
+ functions read (``build.stats[stat]``); ``position``: ``identity``, ``stack``,
66
+ ``dodge``… (:data:`~plot_io.layers.positions.NAMES`), asked of the layer's tables
67
+ (:func:`ask`). ``occupy``, ``position`` and the other options (they go to the layer)
68
+ may be functions of the series."""
69
+
70
+ __slots__ = ("geom", "paint", "table", "aes", "when", "key", "occupy", "drawn", "name",
71
+ "per_set", "stat", "position", "turn", "options")
72
+
73
+ def __init__(self, geom: str, paint: tuple[str, dict[str, Any]] | str,
74
+ table: str = "data", aes: dict[str, str] | None = None, when: Any = None,
75
+ key: Any = True, occupy: Any = True, drawn: bool = True,
76
+ name: str | None = None, per_set: bool = False, stat: str = "identity",
77
+ position: str | Callable[[Any], str] = "identity", turn: bool = False,
78
+ **options: Any) -> None:
79
+ if geom != "errors" and geom not in GEOMS:
80
+ raise ValueError(f"unknown geom {geom!r} (geoms: {', '.join(GEOMS)}, errors)")
81
+ self.geom = geom
82
+ self.paint = (paint, {}) if isinstance(paint, str) else paint
83
+ self.table = table
84
+ self.aes = aes
85
+ self.when = when
86
+ self.key = key
87
+ self.occupy = occupy
88
+ self.drawn = drawn
89
+ self.name = name
90
+ self.per_set = per_set
91
+ self.stat = stat
92
+ self.position = position
93
+ self.turn = turn
94
+ self.options = options
95
+
96
+ def __repr__(self) -> str:
97
+ return f"Lay({self.geom!r}, {self.paint[0]!r}, table={self.table!r})"
98
+
99
+
100
+ class Spec:
101
+ """A plot type as data (see the module docstring).
102
+
103
+ * ``layers``: the :class:`Lay` list, in drawing order;
104
+ * ``tables``: ``name → fn(series, build)`` giving a :class:`Table`, a list of them (one
105
+ per set) or ``None`` (the layers drawing it are left out); ``"data"`` defaults to the
106
+ columns ``x`` and ``y``;
107
+ * ``entries``: ``"name"`` (one entry, the ``name`` setting) or ``"sets"`` (one per set
108
+ of a dict of sets, or per text of a ``name`` tuple, else the ``name``); ``key``: the options of the entries' ``key``
109
+ paint (their ``(fill, stroke)``);
110
+ * ``color_count``: the plot's colors taken, ``"one"``, or one per ``"sets"``, unless
111
+ ``color`` is set;
112
+ * ``zero``: data roles that include 0 under ``include_zero="auto"`` (or a function of
113
+ the series giving them); ``titles``: data
114
+ arguments → data role of their axis titles (default ``x`` and ``y``);
115
+ * ``color_sources`` (a condition, :func:`holds`, or ``"items"``: numbers per item in
116
+ ``color``), ``size_sources``: the color and size settings mapped on the series' axes;
117
+ * ``slots``: the data argument of numeric slot positions of a categorical type (the
118
+ category axis: its categories, or the positions widened by half a slot);
119
+ * ``finish``: ``fn(series, build)`` run last (what no field declares);
120
+ * ``items``: the noun of the sets in messages.
121
+ """
122
+
123
+ __slots__ = ("layers", "tables", "entries", "key", "color_count", "zero", "titles",
124
+ "color_sources", "size_sources", "slots", "finish", "items")
125
+
126
+ def __init__(self, layers: list[Lay], tables: dict[str, Callable[..., Any]] | None = None,
127
+ entries: str = "name", key: dict[str, Any] | None = None,
128
+ color_count: str = "one",
129
+ zero: tuple[str, ...] | Callable[[Any], tuple[str, ...]] = (),
130
+ titles: dict[str, str] | None = None, color_sources: Any = False,
131
+ size_sources: bool = False, slots: str | None = None,
132
+ finish: Callable[[Any, "Build"], None] | None = None,
133
+ items: str = "sets") -> None:
134
+ names = [la.name for la in layers if la.name]
135
+ for la in layers:
136
+ for k, v in la.paint[1].items():
137
+ if isinstance(v, str) and v.startswith("@") and v[1:] not in names:
138
+ raise ValueError(f"layer {la!r}: paint option {k}={v!r} names no layer")
139
+ self.layers = layers
140
+ self.tables = {"data": _data_table, **(tables or {})}
141
+ self.entries = entries
142
+ self.key = key or {}
143
+ self.color_count = color_count
144
+ self.zero = zero
145
+ self.titles = titles
146
+ self.color_sources = color_sources
147
+ self.size_sources = size_sources
148
+ self.slots = slots
149
+ self.finish = finish
150
+ self.items = items
151
+
152
+ def check(self, kind: str) -> None:
153
+ """Check the paint roles and their options, and the layer options of each geom
154
+ (``kind``: the type, named in the error). Run at a type's first build: roles may
155
+ be registered after its spec."""
156
+ import inspect
157
+ from .geoms import LAYER_OPTIONS
158
+ from .paint import ROLES
159
+ from .stats import STATS
160
+ for lay in self.layers:
161
+ if lay.stat != "identity" and lay.stat not in STATS:
162
+ raise ValueError(f"{kind}: layer {lay!r}: unknown stat {lay.stat!r} (stats: "
163
+ f"identity, {', '.join(sorted(STATS))})")
164
+ if not callable(lay.position):
165
+ check_position(f"{kind}: layer {lay!r}", lay.position)
166
+ paints = [(la, la.paint[0], set(la.paint[1]) | ({"index"} if la.per_set else set()))
167
+ for la in self.layers] + [(None, "key", set(self.key) | {"index"})]
168
+ for la, role, given in paints:
169
+ where = f"{kind}: " + ("legend key" if la is None else f"layer {la!r}")
170
+ if role not in ROLES:
171
+ raise ValueError(f"{where}: unknown paint role {role!r} (roles: "
172
+ f"{', '.join(sorted(ROLES))})")
173
+ params = inspect.signature(ROLES[role]).parameters
174
+ if not any(p.kind is p.VAR_KEYWORD for p in params.values()):
175
+ bad = sorted(given - set(list(params)[3:]))
176
+ if bad:
177
+ raise ValueError(f"{where}: paint role {role!r} takes no option "
178
+ f"{bad[0]!r}")
179
+ for la in self.layers:
180
+ known = (("skip", "order") if la.geom == "errors"
181
+ else GEOMS[la.geom].OPTIONS + LAYER_OPTIONS)
182
+ bad = sorted(set(la.options) - set(known))
183
+ if bad:
184
+ raise ValueError(f"{kind}: layer {la!r}: geom {la.geom!r} takes no option "
185
+ f"{bad[0]!r} (options: {', '.join(known)})")
186
+
187
+
188
+ def check_position(kind: str, name: Any) -> str:
189
+ """A position name (:data:`~plot_io.layers.positions.NAMES`), else a ``ValueError``."""
190
+ from .positions import NAMES
191
+ if name not in NAMES:
192
+ raise ValueError(f"{kind}: unknown position {name!r} (positions: "
193
+ f"{', '.join(NAMES)})")
194
+ return name
195
+
196
+
197
+ def _data_table(s: Any, b: "Build") -> Table:
198
+ c = s.cols
199
+ return Table(x=c["x"], y=c["y"])
200
+
201
+
202
+ class Build:
203
+ """The state of one run of a spec: the series, its call, ``flip`` (orientation), the
204
+ column functions of positions given as categories (``at``), the tables, the layers'
205
+ paints by name."""
206
+
207
+ def __init__(self, series: Any, call: Call, flip: bool) -> None:
208
+ self.series = series
209
+ self.call = call
210
+ self.flip = flip
211
+ self.at: dict[str, Any] = {}
212
+ self.tables: dict[str, Any] = {}
213
+ #: the output of each stat the layers declare (:func:`~plot_io.layers.stats.run`)
214
+ self.stats: dict[str, Any] = {}
215
+ self.paints: dict[str, Paint] = {}
216
+ #: the key layers in order, with the set they belong to (-1: every set)
217
+ self.symbol: list[tuple[int, Layer]] = []
218
+
219
+ def options(self, opts: dict[str, Any]) -> dict[str, Any]:
220
+ """Paint options: ``"@name"`` the paint of layer ``name``, ``"#name"`` the table
221
+ ``name``, a function of the series called."""
222
+ out = {}
223
+ for n, v in opts.items():
224
+ if isinstance(v, str) and v.startswith("@"):
225
+ v = self.paints[v[1:]]
226
+ elif isinstance(v, str) and v.startswith("#"):
227
+ v = self.tables[v[1:]]
228
+ elif callable(v):
229
+ v = v(self.series)
230
+ out[n] = v
231
+ return out
232
+
233
+ def paint(self, lay: "Lay", index: int | None = None) -> Paint:
234
+ """The paint of a layer (``index``: of its table among a list's)."""
235
+ role, opts = lay.paint
236
+ opts = self.options(opts)
237
+ if index is not None:
238
+ opts["index"] = index
239
+ return Paint(self.series, role, **opts)
240
+
241
+ def role(self, data_role: str) -> str:
242
+ """The plot axis a data role is drawn on."""
243
+ if not self.flip:
244
+ return data_role
245
+ return {"x": "y", "y": "x"}.get(data_role, data_role)
246
+
247
+
248
+ #: the types whose spec was checked (:meth:`Spec.check`)
249
+ _CHECKED: set[type] = set()
250
+
251
+
252
+ def build(series: Any) -> Call:
253
+ """Run the spec of ``series``' type: its :class:`Call`."""
254
+ from ..presets._util import columns_meta, keyed
255
+ from .paint import color_sources, size_sources
256
+ from .stats import run as run_stat
257
+ spec: Spec = type(series).SPEC
258
+ if type(series) not in _CHECKED:
259
+ spec.check(type(series).__name__)
260
+ _CHECKED.add(type(series))
261
+ call = Call(series)
262
+ flip = flipped(series)
263
+ b = Build(series, call, flip)
264
+ b.at = keyed(call, series.cols.keys, flip, [])
265
+ lays = [la for la in spec.layers if holds(series, la.when)]
266
+ for name, fn in spec.tables.items():
267
+ drawing = [la for la in lays if la.table == name]
268
+ if drawing:
269
+ for la in drawing:
270
+ if la.stat != "identity" and la.stat not in b.stats:
271
+ b.stats[la.stat] = run_stat(series, la.stat)
272
+ b.tables[name] = fn(series, b)
273
+ asked: set[int] = set()
274
+ for la in lays:
275
+ got = b.tables.get(la.table)
276
+ if got is not None:
277
+ ts = [t for t in (got if isinstance(got, list) else [got]) if id(t) not in asked]
278
+ asked.update(id(t) for t in ts)
279
+ ask(b, check_position(series.kind, _value(la.position, series)), ts)
280
+ for t in _all_tables(b.tables):
281
+ for name, fn in b.at.items():
282
+ if name in t and name not in t.lazy:
283
+ t.lazy[name] = fn
284
+ for lay in lays:
285
+ if lay.name:
286
+ b.paints[lay.name] = b.paint(lay)
287
+ for group in _groups(lays):
288
+ if group[0].geom == "errors":
289
+ call.layers += _errors(b, group[0])
290
+ continue
291
+ got = b.tables.get(group[0].table)
292
+ if got is None:
293
+ continue
294
+ many = isinstance(got, list)
295
+ for k, t in enumerate(got if many else [got]):
296
+ for lay in group:
297
+ _add(b, lay, t, k)
298
+ _entries(b, spec)
299
+ if spec.color_count == "sets":
300
+ call.color_count = 0 if series.is_set("color") else len(series.cols.sets)
301
+ else:
302
+ call.color_count = 0 if series.is_set("color") else 1
303
+ if spec.color_sources == "items":
304
+ call.color_sources = _item_colors(series)
305
+ elif holds(series, spec.color_sources):
306
+ call.color_sources = color_sources(series)
307
+ if spec.size_sources:
308
+ call.size_sources = size_sources(series)
309
+ call.zero = {b.role(r) for r in _value(spec.zero, series)}
310
+ titles = {"x": "y", "y": "x"} if flip else {}
311
+ if spec.titles is not None:
312
+ titles = {k: b.role(v) for k, v in spec.titles.items()}
313
+ columns_meta(call, titles)
314
+ if spec.slots is not None:
315
+ _slots(b, series.cols[spec.slots])
316
+ if spec.finish is not None:
317
+ spec.finish(series, b)
318
+ return call
319
+
320
+
321
+ def ask(b: Build, position: str, tables: list[Table]) -> None:
322
+ """Ask ``position`` of the tables of a layer holding ``heights`` (per key: ``keys``)
323
+ that have no request of their own: ``stack`` piles them on the plot's stacked tables of
324
+ the type and orientation (``stack="normalize"``: as shares); ``dodge`` shares each key's
325
+ slot with the call's other sets; else each is drawn from 0 (``bottom``, ``top``).
326
+ Tables without ``heights`` are the type's to place."""
327
+ s = b.series
328
+ for k, t in enumerate(tables):
329
+ if "heights" not in t or t.stack is not None or "top" in t:
330
+ continue
331
+ if position == "stack":
332
+ if "keys" not in t:
333
+ raise ValueError(f"{s.kind}: position 'stack' needs keys (the table has "
334
+ f"{', '.join(t)})")
335
+ mode = s.resolve("stack") if "stack" in s.SETTINGS else True
336
+ t.stack = ((s.kind, b.flip), mode or True)
337
+ continue
338
+ if position == "dodge" and t.dodge is None and "keys" in t:
339
+ from .positions import Dodge
340
+ t.dodge = Dodge(s, (s.kind, id(s)), b.flip, t["keys"], token=k)
341
+ h = t["heights"]
342
+ t["bottom"], t["top"] = [0.0] * len(h), list(h)
343
+
344
+
345
+ def _item_colors(s: Any) -> list[tuple[Any, Any]]:
346
+ """``(color axis, numbers)`` of numbers per item (a list, or a dict by category) in
347
+ ``color``."""
348
+ from array import array
349
+ from ..mapped import color_axis_of
350
+ from ..presets._util import per_category
351
+ from ..values import Values
352
+ c = s.resolve("color")
353
+ if not isinstance(c, Values):
354
+ return []
355
+ return [(color_axis_of(s), array("d", per_category(s, s.cols.categories, c.data,
356
+ getattr(s, "ITEMS", "items"))))]
357
+
358
+
359
+ def _slots(b: Build, positions: Any) -> None:
360
+ """The category axis: the categories, or numeric ``positions`` widened by half a
361
+ slot."""
362
+ from array import array
363
+ from .positions import slot_extent
364
+ s, call = b.series, b.call
365
+ cat = b.role("x")
366
+ if positions is None:
367
+ call.cats[cat] = list(s.cols.categories)
368
+ return
369
+ time = cat in call.times
370
+ call.extent_of[cat] = lambda scale: slot_extent(s, positions, time, scale)
371
+ call.extra[cat] = array("d", positions)
372
+
373
+
374
+ def _groups(lays: list[Lay]) -> list[list[Lay]]:
375
+ """The layers in drawing groups: consecutive per-set layers of one table list are
376
+ drawn set by set (each set's layers together)."""
377
+ out: list[list[Lay]] = []
378
+ for la in lays:
379
+ prev = out[-1][-1] if out else None
380
+ if prev is not None and la.per_set and prev.per_set and la.table == prev.table:
381
+ out[-1].append(la)
382
+ else:
383
+ out.append([la])
384
+ return out
385
+
386
+
387
+ def _add(b: Build, lay: Lay, t: Table, k: int) -> None:
388
+ """The layer of ``lay`` drawing table ``t`` (``k``: its index in its list)."""
389
+ s = b.series
390
+ paint = b.paints[lay.name] if lay.name else b.paint(lay, k if lay.per_set else None)
391
+ options = {n: _value(v, s) for n, v in lay.options.items()}
392
+ layer = Layer(GEOMS[lay.geom], t, aes=lay.aes, paint=paint, flip=b.flip != lay.turn,
393
+ stat=lay.stat,
394
+ position=_value(lay.position, s), occupy=_value(lay.occupy, s),
395
+ key=bool(lay.key) and (lay.key != "first" or k == 0), **options)
396
+ if layer.key:
397
+ b.symbol.append((k if lay.per_set else -1, layer))
398
+ if lay.drawn:
399
+ b.call.layers.append(layer)
400
+
401
+
402
+ def _value(v: Any, series: Any) -> Any:
403
+ """A spec value, or a function of the series giving it."""
404
+ return v(series) if callable(v) else v
405
+
406
+
407
+ def _all_tables(tables: dict[str, Any]) -> list[Table]:
408
+ out = []
409
+ for t in tables.values():
410
+ if isinstance(t, list):
411
+ out.extend(t)
412
+ elif t is not None:
413
+ out.append(t)
414
+ return out
415
+
416
+
417
+ def _errors(b: Build, lay: Lay) -> list[Layer]:
418
+ """The error bars of ``x_error`` and ``y_error`` (in ``order``; ``skip``: ``(role,
419
+ condition)``, a role whose bars another layer draws) at the data table's points."""
420
+ from ..presets.errors import error_layers, errors_of
421
+ s = b.series
422
+ source = b.paint(lay)
423
+ data = b.tables["data"]
424
+ xs = b.at.get("x", data["x"])
425
+ y = data["y"]
426
+ ys = b.at.get("y", y[0] if isinstance(y, list) else y)
427
+ skip = lay.options.get("skip")
428
+ out: list[Layer] = []
429
+ for role in lay.options.get("order", ("x", "y")):
430
+ e = errors_of(s, f"{role}_error")
431
+ if e is None:
432
+ continue
433
+ # one Errors per set (a summary of several sets) at each set's points
434
+ each = list(zip(e, y)) if isinstance(e, list) else [(e, ys)]
435
+ for e, _ in each:
436
+ e.bind(len(s.cols["x"]), s, f"{role}_error")
437
+ if role in s.cols.keys:
438
+ raise ValueError(f"{s.kind}: {role}_error needs numbers in {role}, not "
439
+ "categories")
440
+ if skip is not None and skip[0] == role and holds(s, skip[1]):
441
+ continue
442
+ for e, at in each:
443
+ out += error_layers(s, e, xs, at, role == "x", source, b.flip)
444
+ return out
445
+
446
+
447
+ def _entries(b: Build, spec: Spec) -> None:
448
+ """The call's legend entries, symbols as data."""
449
+ s = b.series
450
+ if spec.entries == "sets" and s.cols.named:
451
+ for i, (k, _) in enumerate(s.cols.sets):
452
+ layers = [la for j, la in b.symbol if j in (-1, i)]
453
+ b.call.entries.append(_entry(b, str(k), layers, spec, i))
454
+ return
455
+ name = s.resolve("name")
456
+ if spec.entries == "sets" and isinstance(name, tuple):
457
+ n = len(s.cols.sets)
458
+ if len(name) != n:
459
+ raise ValueError(f"{s.kind}: name has {len(name)} texts for {n} {spec.items}")
460
+ for i, label in enumerate(name):
461
+ layers = [la for j, la in b.symbol if j in (-1, i)]
462
+ b.call.entries.append(_entry(b, label, layers, spec, i))
463
+ return
464
+ if name is not None:
465
+ layers = [la for j, la in b.symbol if j in (-1, 0)]
466
+ b.call.entries.append(_entry(b, name, layers, spec, 0 if spec.entries == "sets"
467
+ else None))
468
+
469
+
470
+ def _entry(b: Build, label: Any, layers: list[Layer], spec: Spec, index: Any) -> Entry:
471
+ opts = b.options(spec.key)
472
+ if index is not None:
473
+ opts["index"] = index
474
+ return entry(b.series, label, layers, **opts)
475
+
476
+
477
+ def entry(series: Any, label: Any, layers: list[Layer], **key: Any) -> Entry:
478
+ """A legend entry whose symbol is the parts of ``layers`` (their key layers), with
479
+ the options ``key`` of its ``key`` paint (its simple symbol's colors)."""
480
+ parts, paints = [], []
481
+ for la in layers:
482
+ part = la.part() if la.key else None
483
+ if part is not None:
484
+ parts.append(part)
485
+ paints.append(la.paint)
486
+ return Entry(label, tuple(parts), tuple(paints), Paint(series, "key", **key))
487
+
488
+
489
+ class _Map:
490
+ """A mapper placing data at ``a`` px per unit (:class:`_SlotContext`)."""
491
+
492
+ def __init__(self, a: float) -> None:
493
+ self.a = a
494
+
495
+ def __call__(self, v: float) -> float:
496
+ return v * self.a
497
+
498
+ position = __call__
499
+
500
+ def many(self, v: Any) -> Any:
501
+ from array import array
502
+ return array("d", (x * self.a for x in v))
503
+
504
+
505
+ class _State:
506
+ __slots__ = ("categories", "scale")
507
+
508
+ def __init__(self, categories: list[Any]) -> None:
509
+ self.categories, self.scale = categories, None
510
+
511
+
512
+ class _SlotContext:
513
+ """A drawing context before layout: each category at its index, lengths in px
514
+ negligible (a slot is 1e9 px), so lazy columns put items within their slots."""
515
+
516
+ def __init__(self, cats: dict[str, list[Any]]) -> None:
517
+ self.root = self
518
+ self.x, self.y = _State(cats.get("x", [])), _State(cats.get("y", []))
519
+ self.xmap = self.ymap = _Map(1e9)
520
+ self.cycle: tuple = ()
521
+ self.rect = (0.0, 0.0, 1.0, 1.0)
522
+
523
+ def color(self, value: Any) -> Any:
524
+ from ..color.values import resolve_color
525
+ return resolve_color(value, self.cycle)
526
+
527
+
528
+ def top(call: Call, lo: float, hi: float, role: str, cats: dict[str, list[Any]]) -> float:
529
+ """The highest finite value ``call``'s layers draw on the plot axis ``role`` (``"y"``
530
+ or ``"x"``) at positions in [``lo``, ``hi``] on the other axis (the middle of each
531
+ item's position columns; category units on a category axis, ``cats``: the axes'
532
+ categories); ``-inf`` when none. Columns that need the placed axes are read with each
533
+ category at its index; a layer they fail for counts whole."""
534
+ import math
535
+ other = "x" if role == "y" else "y"
536
+ best = -math.inf
537
+ ctx: Any = None
538
+ for layer in call.layers:
539
+ vch = layer.geom.CHANNELS.get(layer.frame_role(role), ())
540
+ pch = layer.geom.CHANNELS.get(layer.frame_role(other), ())
541
+ if not vch:
542
+ continue
543
+ if layer.table.lazy:
544
+ ctx = ctx or _SlotContext(cats)
545
+ try:
546
+ layer = layer.columns(ctx)
547
+ except Exception: # noqa: BLE001 — counted whole below
548
+ pch = ()
549
+ vals = [_arrays(layer.col(c)) for c in vch]
550
+ poss = [_arrays(layer.col(c)) for c in pch] if pch else []
551
+ for k in range(max((len(v) for v in vals), default=0)):
552
+ vs = [v[k] if k < len(v) else None for v in vals]
553
+ ps = [p[k] if k < len(p) else None for p in poss]
554
+ best = max(best, _top_rows(vs, ps, lo, hi))
555
+ return best
556
+
557
+
558
+ def _arrays(v: Any) -> list[Any]:
559
+ """The arrays of a channel (several for a list of arrays; a constant as itself)."""
560
+ if v is None:
561
+ return []
562
+ if isinstance(v, list) and v and hasattr(v[0], "__len__"):
563
+ return v
564
+ return [v]
565
+
566
+
567
+ def _top_rows(vs: list[Any], ps: list[Any], lo: float, hi: float) -> float:
568
+ import math
569
+ best = -math.inf
570
+ n = max((len(v) for v in vs + ps if hasattr(v, "__len__")), default=1)
571
+
572
+ def at(c: Any, i: int) -> float:
573
+ if c is None:
574
+ return math.nan
575
+ return float(c[i]) if hasattr(c, "__len__") else float(c)
576
+ for i in range(n):
577
+ if ps:
578
+ p = [at(c, i) for c in ps]
579
+ p = [x for x in p if math.isfinite(x)]
580
+ if not p or not lo <= (min(p) + max(p)) / 2 <= hi:
581
+ continue
582
+ for c in vs:
583
+ v = at(c, i)
584
+ if v > best and math.isfinite(v):
585
+ best = v
586
+ return best