plot-io 0.12.0__py3-none-win_amd64.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-x86_64.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/columns.py ADDED
@@ -0,0 +1,614 @@
1
+ """Data intake: the data arguments of a plot type's call as columns.
2
+
3
+ One rule for every plot type:
4
+
5
+ * a column is a sequence, a 1-D buffer, or a pandas or polars series (nullable arrays
6
+ included: their missing values are NaN); ``None`` and NaN are missing values; a
7
+ generator is read once;
8
+ * numeric slots (values, heights, ``y``, weights) take numbers: text is refused, naming
9
+ the argument (``"1"`` is not read as 1); durations (``timedelta``, ``timedelta64``)
10
+ are read as seconds;
11
+ * position slots (``x``, ``y`` of the (x, y) types) take numbers or categories: text,
12
+ or a pandas or polars categorical (its order kept), gives :class:`Keys`;
13
+ * several columns: a dict ``{name: column}``, a data frame (columns by name), a 2-D
14
+ buffer (its **columns** are the variables, as in numpy and pandas), or a Python list or
15
+ tuple of sequences (each a column).
16
+
17
+ :class:`Columns` reads the data arguments of one call once, from the type's ``DATA``
18
+ declaration (each argument with its slot kind), into columns and groups of rows.
19
+ """
20
+
21
+ import array
22
+ import datetime as _dt
23
+ import math
24
+ import numbers as _numbers
25
+ from typing import Any
26
+
27
+ from vec_io.arrays import buffer_shape, doubles, grid
28
+
29
+ from .validators import typed
30
+
31
+ __all__ = ["Keys", "Columns", "Group", "KINDS", "numbers", "positions", "is_categorical",
32
+ "is_frame", "is_several", "sets", "named_sets", "weights"]
33
+
34
+ _NAN = math.nan
35
+
36
+
37
+ def is_frame(v: Any) -> bool:
38
+ """Whether ``v`` is a data frame (pandas, polars: named columns)."""
39
+ shape = getattr(v, "shape", None)
40
+ return (hasattr(v, "columns") and not isinstance(v, dict) and shape is not None
41
+ and len(shape) == 2 and hasattr(v, "__getitem__"))
42
+
43
+
44
+ def _shape(v: Any) -> tuple[int, ...] | None:
45
+ """The shape of a numeric buffer, ``None`` for other values (durations included)."""
46
+ if isinstance(v, (str, bytes, list, tuple, dict)) \
47
+ or getattr(getattr(v, "dtype", None), "kind", None) == "m":
48
+ return None
49
+ return buffer_shape(v)
50
+
51
+
52
+ def _missing(item: Any) -> bool:
53
+ return item is None or type(item).__name__ in ("NAType", "NaTType", "NoneType")
54
+
55
+
56
+ def _plain(v: Any) -> Any:
57
+ """A pandas or polars series (or pandas array) as a numpy array, missing values of a
58
+ nullable numeric type as NaN, durations as seconds; other values unchanged."""
59
+ if getattr(getattr(v, "dtype", None), "kind", None) == "m":
60
+ return _durations(v)
61
+ if not hasattr(v, "to_numpy") or hasattr(v, "__array_interface__"):
62
+ return v
63
+ try:
64
+ return v.to_numpy(dtype=float, na_value=_NAN)
65
+ except (TypeError, ValueError):
66
+ return v.to_numpy()
67
+
68
+
69
+ def _durations(v: Any) -> array.array:
70
+ """A ``timedelta64`` array or series as seconds (``NaT``: NaN)."""
71
+ if hasattr(v, "to_numpy") and not hasattr(v, "__array_interface__"):
72
+ v = v.to_numpy()
73
+ ns = v.astype("timedelta64[ns]").astype("int64").tolist()
74
+ return array.array("d", (_NAN if n == -(2 ** 63) else n / 1e9 for n in ns))
75
+
76
+
77
+ def numbers(v: Any, what: str) -> array.array:
78
+ """One numeric column as ``array('d')`` (missing values NaN); text refused."""
79
+ if isinstance(v, array.array) and v.typecode == "d":
80
+ return v
81
+ if isinstance(v, (str, bytes)):
82
+ raise ValueError(f"{what} must be numbers, not text {v!r}")
83
+ if isinstance(v, Keys):
84
+ raise ValueError(f"{what} must be numbers, not categories")
85
+ v = _plain(v)
86
+ shape = _shape(v)
87
+ if shape is not None:
88
+ if len(shape) != 1:
89
+ raise ValueError(f"{what} must be 1-D, not of shape {shape}")
90
+ return doubles(v)
91
+ if hasattr(v, "tolist") and not isinstance(v, (list, tuple)):
92
+ v = v.tolist()
93
+ if isinstance(v, (list, tuple)):
94
+ try:
95
+ return array.array("d", v) # numbers only (no text, no None)
96
+ except TypeError:
97
+ pass
98
+ out = array.array("d")
99
+ append = out.append
100
+ for item in v:
101
+ if isinstance(item, float):
102
+ append(item)
103
+ elif isinstance(item, _numbers.Real) and not isinstance(item, bool):
104
+ append(float(item))
105
+ elif _missing(item):
106
+ append(_NAN)
107
+ elif isinstance(item, _dt.timedelta):
108
+ append(item.total_seconds())
109
+ elif isinstance(item, (str, bytes)):
110
+ raise ValueError(f"{what} must be numbers, not text: {item!r}")
111
+ else:
112
+ try:
113
+ append(float(item))
114
+ except (TypeError, ValueError):
115
+ raise ValueError(f"{what} must be numbers, not {type(item).__name__}: "
116
+ f"{item!r}") from None
117
+ return out
118
+
119
+
120
+ class Keys:
121
+ """Positions given as categories: the key of each point (``None``: missing) and the
122
+ categories in order (a categorical's order, else first appearance)."""
123
+
124
+ __slots__ = ("keys", "levels", "own")
125
+
126
+ def __init__(self, keys: Any, order: Any = None) -> None:
127
+ self.keys = [None if _missing(k) or (isinstance(k, float) and math.isnan(k)) else k
128
+ for k in keys]
129
+ levels = list(order) if order is not None else []
130
+ seen = set(levels)
131
+ for k in self.keys:
132
+ if k is not None and k not in seen:
133
+ seen.add(k)
134
+ levels.append(k)
135
+ self.levels = levels
136
+ index = {k: float(i) for i, k in enumerate(levels)}
137
+ #: each point at its category's index among ``levels`` (before the axes are known)
138
+ self.own = array.array("d", (_NAN if k is None else index[k] for k in self.keys))
139
+
140
+ def __len__(self) -> int:
141
+ return len(self.keys)
142
+
143
+ def __iter__(self) -> Any:
144
+ return iter(self.keys)
145
+
146
+ def __getitem__(self, i: Any) -> Any:
147
+ return self.keys[i]
148
+
149
+ def take(self, idx: list[int]) -> "Keys":
150
+ out = Keys.__new__(Keys)
151
+ out.keys = [self.keys[i] for i in idx]
152
+ out.levels = self.levels
153
+ out.own = array.array("d", (self.own[i] for i in idx))
154
+ return out
155
+
156
+ def on(self, categories: list[Any]) -> array.array:
157
+ """Each point at its category's index on an axis with ``categories``."""
158
+ index: dict[Any, float] = {}
159
+ for i, c in enumerate(categories):
160
+ index.setdefault(c, float(i))
161
+ return array.array("d", (_NAN if k is None else index.get(k, _NAN)
162
+ for k in self.keys))
163
+
164
+ def __repr__(self) -> str:
165
+ return f"Keys({self.levels!r})"
166
+
167
+
168
+ def _order(v: Any) -> list[Any] | None:
169
+ """The categories of a pandas or polars categorical, in order (else ``None``)."""
170
+ cat = getattr(v, "cat", None)
171
+ if cat is not None and hasattr(cat, "categories"):
172
+ try:
173
+ return list(cat.categories)
174
+ except TypeError:
175
+ pass
176
+ dtype: Any = getattr(v, "dtype", None)
177
+ if dtype is not None and hasattr(dtype, "categories") and (
178
+ hasattr(v, "codes") or str(dtype).startswith("Enum")):
179
+ return list(dtype.categories) # pandas Categorical, polars Enum
180
+ if str(dtype).startswith(("Categorical", "Enum")):
181
+ try: # polars
182
+ return list(v.cat.get_categories())
183
+ except (AttributeError, TypeError):
184
+ pass
185
+ return None
186
+
187
+
188
+ def is_categorical(v: Any) -> bool:
189
+ """Whether a position column holds categories: text, or a categorical."""
190
+ if isinstance(v, Keys) or _order(v) is not None:
191
+ return True
192
+ kind = getattr(getattr(v, "dtype", None), "kind", None)
193
+ if kind in ("U", "S"):
194
+ return True
195
+ if kind == "O" or isinstance(v, (list, tuple)):
196
+ items = v.tolist() if kind == "O" else v
197
+ some = False
198
+ for k in items:
199
+ if isinstance(k, str):
200
+ some = True
201
+ elif not (_missing(k) or (isinstance(k, float) and math.isnan(k))):
202
+ return False
203
+ return some
204
+ return False
205
+
206
+
207
+ def positions(v: Any, what: str) -> Any:
208
+ """A position column: ``array('d')`` of numbers, or :class:`Keys` of categories."""
209
+ if isinstance(v, Keys):
210
+ return v
211
+ if not isinstance(v, (list, tuple, array.array)) and _shape(v) is None and not hasattr(
212
+ v, "to_numpy") and not hasattr(v, "dtype"):
213
+ v = list(v) # a generator: read once
214
+ if is_categorical(v):
215
+ order = _order(v)
216
+ if hasattr(v, "tolist") and not isinstance(v, (list, tuple)):
217
+ v = v.tolist()
218
+ return Keys(v, order)
219
+ return numbers(v, what)
220
+
221
+
222
+ def is_several(v: Any) -> bool:
223
+ """Whether ``v`` holds several columns (a dict, a data frame, a 2-D buffer, a list or
224
+ tuple of sequences)."""
225
+ if isinstance(v, dict) or is_frame(v):
226
+ return True
227
+ shape = _shape(v)
228
+ if shape is not None:
229
+ return len(shape) == 2
230
+ return isinstance(v, (list, tuple)) and bool(v) and all(
231
+ hasattr(x, "__len__") and not isinstance(x, (str, bytes)) for x in v)
232
+
233
+
234
+ def named_sets(v: Any, what: str) -> tuple[list[tuple[Any, Any]], bool]:
235
+ """``([(name, column)], named)`` of several columns (as :func:`is_several`), columns
236
+ left as given (``named``: by a dict's keys or a frame's column names; else ``0, 1,
237
+ …``); one column otherwise."""
238
+ if isinstance(v, dict):
239
+ return list(v.items()), True
240
+ if is_frame(v):
241
+ return [(c, v[c]) for c in v.columns], True
242
+ shape = _shape(v)
243
+ if shape is not None and len(shape) == 2:
244
+ h, w, _, flat = grid(v)
245
+ return [(j, flat[j::w] if w > 1 else flat) for j in range(w)], False
246
+ if is_several(v):
247
+ return list(enumerate(v)), False
248
+ return [(0, v)], False
249
+
250
+
251
+ def sets(v: Any, what: str) -> tuple[list[tuple[Any, array.array]], bool]:
252
+ """``([(name, numbers)], named)`` of one column or several (see the module)."""
253
+ items, named = named_sets(v, what)
254
+ one = len(items) == 1 and not named and not is_several(v)
255
+ return [(k, numbers(c, what if one else f"{what} {k!r}")) for k, c in items], named
256
+
257
+
258
+ @typed("Column | Mapping[Any, Any]")
259
+ def weights(v: Any) -> Any:
260
+ """A ``weights`` setting: one column, or one per set (a dict or a frame by set name,
261
+ else a list in set order)."""
262
+ if not is_several(v):
263
+ return numbers(v, "weights")
264
+ items, named = named_sets(v, "weights")
265
+ out = [(k, numbers(c, f"weights {k!r}")) for k, c in items]
266
+ return dict(out) if named else [c for _, c in out]
267
+
268
+
269
+ #: The slot kinds of a type's ``DATA`` declaration (``{argument: kind}``, in reading
270
+ #: order).
271
+ KINDS = {
272
+ "position": "numbers or categories (x and y of the (x, y) types)",
273
+ "numbers": "numbers",
274
+ "sets": "one column or several, along x (line y, area ys)",
275
+ "samples": "one set of values or several; dates allowed (hist, kde, ecdf)",
276
+ "categories": "value sets by category, dates allowed; long data: values flat with the "
277
+ "category of each in x (y when horizontal)",
278
+ "heights": "heights at x: x and heights, a dict {x: height}, or heights alone",
279
+ "slots": "numeric positions, one per group (box, violin, strip, swarm)",
280
+ "curve": "a constant or a column along x (fill)",
281
+ "wedges": "numbers, or a dict of them by name (pie)",
282
+ "raw": "kept as given (grids, coordinates)",
283
+ "cells": "kept as given, categories never read as positions (dots' long data)",
284
+ }
285
+
286
+
287
+ def _length(v: Any) -> int | None:
288
+ if v is None or isinstance(v, (str, bytes, dict, int, float)):
289
+ return None
290
+ try:
291
+ return len(v)
292
+ except TypeError:
293
+ return None
294
+
295
+
296
+ def _gone(v: Any) -> bool:
297
+ return v is None or (isinstance(v, float) and math.isnan(v))
298
+
299
+
300
+ def _seconds(v: Any) -> tuple[Any, Any] | None:
301
+ """Dates in one array, a dict or a list of arrays as seconds, with the time zone of
302
+ the first set (``None`` when ``v`` holds no dates)."""
303
+ from . import times
304
+ if isinstance(v, dict):
305
+ items = list(v.values())
306
+ elif isinstance(v, (list, tuple)) and v and all(
307
+ hasattr(x, "__len__") and not isinstance(x, str) for x in v):
308
+ items = list(v)
309
+ else:
310
+ return times.to_seconds(v) if times.is_time_like(v) else None
311
+ if not any(times.is_time_like(x) for x in items):
312
+ return None
313
+ out = [times.to_seconds(x) for x in items]
314
+ tz = next((z for _, z in out if z is not None), None)
315
+ if isinstance(v, dict):
316
+ return {k: s for k, (s, _) in zip(v, out)}, tz
317
+ return [s for s, _ in out], tz
318
+
319
+
320
+ class Group:
321
+ """Rows of a call sharing a category and a set: their columns by argument."""
322
+
323
+ __slots__ = ("category", "set", "data")
324
+
325
+ def __init__(self, category: Any, set: Any, data: dict[str, Any]) -> None:
326
+ self.category = category
327
+ self.set = set
328
+ self.data = data
329
+
330
+ def __repr__(self) -> str:
331
+ return f"Group({self.category!r}, {self.set!r})"
332
+
333
+
334
+ #: The attributes :meth:`Columns._read` sets.
335
+ _READ = ("data", "keys", "groups", "categories", "named", "value", "counted")
336
+
337
+
338
+ class Columns:
339
+ """The data arguments of one call, read once (by ``SeriesMeta``, after ``data=``
340
+ columns and dates) from the type's ``DATA`` declaration (:data:`KINDS`).
341
+
342
+ * ``data``: the columns by argument: ``array('d')`` (positions given as categories at
343
+ their index among their own), a constant, or the argument as given (``raw``);
344
+ * ``keys``: the position arguments given as categories (:class:`Keys`);
345
+ * ``groups``: the rows as :class:`Group` ``(category, set, data)``: one per set of
346
+ wide input (a dict, a frame, a 2-D buffer, a list of sets; never concatenated), one
347
+ per category of long input (grouped once by a stable argsort of the codes);
348
+ * ``categories``: the category of each group (``categories`` slots), of each bar
349
+ (``heights``), of each wedge (``wedges``, when named);
350
+ * ``named``: whether the sets are named (a dict's keys, a frame's columns);
351
+ * ``titles``: the ``data=`` column name of each argument; ``times``: the time zone of
352
+ each argument given as dates; ``orders``: the categorical orders of ``data=``
353
+ columns.
354
+ """
355
+
356
+ def __init__(self, kinds: dict[str, str], args: dict[str, Any], options: dict[str, Any],
357
+ titles: dict[str, Any], orders: dict[str, Any], times: dict[str, Any],
358
+ read: bool = True) -> None:
359
+ self.kinds = kinds
360
+ #: the arguments as read (``data=`` columns, dates as seconds), before parsing
361
+ self.args = args
362
+ self.options = options
363
+ self.titles = titles
364
+ self.orders = orders
365
+ self.times = times
366
+ if read:
367
+ self._read()
368
+
369
+ def __getattr__(self, name: str) -> Any:
370
+ if name in _READ and "kinds" in self.__dict__: # not read yet: read on first use
371
+ self._read()
372
+ return getattr(self, name)
373
+ raise AttributeError(name)
374
+
375
+ def _read(self) -> None:
376
+ self.data: dict[str, Any] = {}
377
+ self.keys: dict[str, Keys] = {}
378
+ self.groups: list[Group] = []
379
+ self.categories: list[Any] = []
380
+ self.named = False
381
+ #: the argument the groups hold
382
+ self.value: str | None = None
383
+ #: ``x`` missing, the second argument's values given in its place: ``x`` is 0, 1, …
384
+ self.counted = False
385
+ names = list(self.kinds)
386
+ self.counted = (len(names) > 1 and names[0] == "x" and self.args.get(names[1]) is None
387
+ and self.kinds[names[1]] in ("numbers", "sets"))
388
+ for name, kind in self.kinds.items():
389
+ getattr(self, "_" + kind)(name)
390
+
391
+ @classmethod
392
+ def build(cls, kinds: dict[str, str], given: dict[str, Any], kw: dict[str, Any],
393
+ titles: dict[str, Any], orders: dict[str, Any], times: dict[str, Any],
394
+ items: str = "sets", read: bool = True) -> "Columns":
395
+ """The columns of a call: ``given`` the data arguments (defaults included), ``kw``
396
+ its other keywords (long data takes its categories from ``x``, ``y`` when
397
+ horizontal); ``items`` names a type's groups in errors. ``read=False``: the
398
+ arguments, read on first use (a call split by ``group=`` or by levels)."""
399
+ args = dict(given)
400
+ options = {"categories": kw.get("categories"), "items": items}
401
+ for name, kind in kinds.items():
402
+ if kind in ("samples", "categories") and args.get(name) is not None:
403
+ converted = _seconds(args[name])
404
+ if converted is not None:
405
+ args[name], times[name] = converted
406
+ if kind == "categories":
407
+ key = "y" if kw.get("orientation") == "horizontal" else "x"
408
+ cats, vals = kw.get(key), args.get(name)
409
+ n = _length(vals)
410
+ if (cats is not None and n is not None and not isinstance(vals, dict)
411
+ and _length(cats) == n):
412
+ args[key] = kw.pop(key)
413
+ options["long"] = key
414
+ if key in titles:
415
+ titles["categories"] = titles.pop(key)
416
+ return cls(kinds, args, options, titles, orders, times, read)
417
+
418
+ def take(self, rows: list[int], n: int) -> "Columns":
419
+ """The columns of ``rows`` (of ``n``: arguments of another length are kept whole),
420
+ read again: categories in their order of appearance among the rows."""
421
+ from .tables import take
422
+ args = {k: (take(v, rows) if _length(v) == n else v) for k, v in self.args.items()}
423
+ return Columns(self.kinds, args, self.options, dict(self.titles), {}, dict(self.times))
424
+
425
+ def __getitem__(self, name: str) -> Any:
426
+ return self.data[name]
427
+
428
+ def __contains__(self, name: str) -> bool:
429
+ return name in self.data
430
+
431
+ def get(self, name: str, default: Any = None) -> Any:
432
+ return self.data.get(name, default)
433
+
434
+ @property
435
+ def sets(self) -> list[tuple[Any, Any]]:
436
+ """``(set, column)`` of each group."""
437
+ key = self.value or ""
438
+ return [(g.set, g.data[key]) for g in self.groups]
439
+
440
+ @property
441
+ def values(self) -> list[Any]:
442
+ """The column of each group."""
443
+ key = self.value or ""
444
+ return [g.data[key] for g in self.groups]
445
+
446
+ def __repr__(self) -> str:
447
+ return f"Columns({list(self.data)}, groups={len(self.groups)})"
448
+
449
+ # -- slot kinds (see KINDS) ---------------------------------------------------------
450
+
451
+ def _pair(self, name: str, c: Any) -> None:
452
+ """A column of an (x, y) call: the other's length checked."""
453
+ if isinstance(c, Keys):
454
+ self.keys[name] = c
455
+ c = c.own
456
+ self.data[name] = c
457
+ other = {"x": "y", "y": "x"}.get(name)
458
+ if other in self.data and self.kinds.get(other) in ("position", "numbers"):
459
+ x, y = (c, self.data[other]) if name == "x" else (self.data[other], c)
460
+ if len(x) != len(y):
461
+ raise ValueError(f"x has {len(x)} values and y {len(y)}")
462
+
463
+ def _position(self, name: str) -> None:
464
+ if name == "x" and (self.counted or "heights" in self.kinds.values()):
465
+ return # read with the next argument
466
+ self._pair(name, positions(self.args[name], name))
467
+
468
+ def _numbers(self, name: str) -> None:
469
+ if self.counted and name != "x":
470
+ y = numbers(self.args["x"], name)
471
+ self._pair("x", numbers(range(len(y)), "x"))
472
+ else:
473
+ y = numbers(self.args[name], name)
474
+ self._pair(name, y)
475
+
476
+ def _sets(self, name: str) -> None:
477
+ items, self.named = sets(self.args["x" if self.counted else name], name)
478
+ if self.counted:
479
+ self.data["x"] = array.array("d", range(len(items[0][1])))
480
+ x = self.data["x"]
481
+ what = name if name == "y" else "a layer"
482
+ for _, r in items:
483
+ if len(r) != len(x):
484
+ raise ValueError(f"x has {len(x)} values and {what} {len(r)}")
485
+ self.value = name
486
+ self.groups = [Group(None, k, {name: v}) for k, v in items]
487
+
488
+ def _samples(self, name: str) -> None:
489
+ items, self.named = sets(self.args[name], name)
490
+ self.value = name
491
+ self.groups = [Group(None, k, {name: v}) for k, v in items]
492
+
493
+ def _categories(self, name: str) -> None:
494
+ values = self.args[name]
495
+ key = self.options.get("long")
496
+ if key is not None:
497
+ values = self._long(numbers(values, name), self.args[key], self.orders.get(key))
498
+ items, self.named = sets(values, name)
499
+ given = self.options.get("categories")
500
+ what = self.options["items"]
501
+ if not self.named and given is not None:
502
+ names = list(given)
503
+ if is_several(values):
504
+ if len(names) != len(items):
505
+ raise ValueError(f"{len(names)} categories for {len(items)} {what}")
506
+ items = list(zip(names, (v for _, v in items)))
507
+ elif names:
508
+ items = [(names[0], items[0][1])]
509
+ self.value = name
510
+ self.groups = [Group(k, None, {name: v}) for k, v in items]
511
+ self.categories = [k for k, _ in items]
512
+
513
+ @staticmethod
514
+ def _long(values: array.array, cats: Any, order: Any) -> dict[Any, array.array]:
515
+ """Flat values by category (``order`` first, then first appearance; rows of a
516
+ missing category dropped, categories without rows left out), each in row order."""
517
+ from bisect import bisect_left, bisect_right
518
+ from . import kernels
519
+ index: dict[Any, int] = {}
520
+ for c in order or []:
521
+ index.setdefault(c, len(index))
522
+ lookup: dict[Any, float] = {}
523
+ for c in dict.fromkeys(cats): # first appearance
524
+ if _gone(c):
525
+ lookup[c] = -1.0
526
+ elif c not in index:
527
+ index[c] = len(index)
528
+ lookup.update((c, float(k)) for c, k in index.items())
529
+ codes = array.array("d", map(lookup.__getitem__, cats))
530
+ gone = codes.count(-1.0)
531
+ if len(index) == 1 and not gone:
532
+ return {next(iter(index)): values} if len(values) else {}
533
+ rows = kernels.argsort(codes)[gone:] # stable; missing (-1) first
534
+ ordered = kernels.take(values, rows) if len(rows) else array.array("d")
535
+ sorted_codes = kernels.take(codes, rows) if len(rows) else array.array("d")
536
+ out: dict[Any, array.array] = {}
537
+ for c, k in index.items():
538
+ lo = bisect_left(sorted_codes, k)
539
+ hi = bisect_right(sorted_codes, k, lo)
540
+ if hi > lo:
541
+ out[c] = ordered[lo:hi]
542
+ return out
543
+
544
+ def _slots(self, name: str) -> None:
545
+ p = self.args.get(name)
546
+ if p is not None:
547
+ p = list(p)
548
+ for v in p:
549
+ if isinstance(v, (str, bytes)):
550
+ raise ValueError(f"{name}: expected numbers (one position per group), "
551
+ f"got {v!r}; categories: a dict {{category: values}} "
552
+ f"or categories=")
553
+ self.data[name] = None if p is None else [float(v) for v in p]
554
+ if p is not None and len(self.data[name]) != len(self.groups):
555
+ raise ValueError(f"{len(self.data[name])} positions for {len(self.groups)} "
556
+ f"{self.options['items']}")
557
+
558
+ def _heights(self, name: str) -> None:
559
+ x, h = self.args["x"], self.args[name]
560
+ if h is None:
561
+ if isinstance(x, dict):
562
+ keys = list(x.keys())
563
+ vals = numbers(list(x.values()), "heights")
564
+ categorical = all(isinstance(k, str) for k in keys)
565
+ else:
566
+ vals = numbers(x, "heights")
567
+ keys = [float(i) for i in range(len(vals))]
568
+ categorical = False
569
+ else:
570
+ vals = numbers(h, "heights")
571
+ pos = positions(x, "x")
572
+ if len(pos) != len(vals):
573
+ raise ValueError(f"{len(pos)} positions for {len(vals)} values")
574
+ categorical = isinstance(pos, Keys)
575
+ if isinstance(pos, Keys):
576
+ self.keys["x"] = pos
577
+ keep = [i for i, k in enumerate(pos.keys) if k is not None] # missing bars
578
+ keys = [pos.keys[i] for i in keep]
579
+ if len(keep) < len(pos):
580
+ vals = array.array("d", [vals[i] for i in keep])
581
+ else:
582
+ keys = list(pos)
583
+ self.categories = keys
584
+ self.data[name] = vals
585
+ self.data["x"] = None if categorical else [float(k) for k in keys]
586
+
587
+ def _curve(self, name: str) -> None:
588
+ if name == "y1":
589
+ y1, y2 = self.args["y1"], self.args.get("y2")
590
+ if is_several(y1) and isinstance(y2, (int, float)) and y2 == 0:
591
+ items, _ = named_sets(y1, "y1")
592
+ if len(items) == 2:
593
+ self.args = {**self.args, "y1": items[0][1], "y2": items[1][1]}
594
+ v = self.args[name]
595
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
596
+ self.data[name] = float(v)
597
+ return
598
+ c = numbers(v, name)
599
+ n = len(self.data["x"])
600
+ if len(c) != n:
601
+ raise ValueError(f"{name} has {len(c)} values for {n} points")
602
+ self.data[name] = c
603
+
604
+ def _wedges(self, name: str) -> None:
605
+ v = self.args[name]
606
+ if isinstance(v, dict):
607
+ self.categories = [str(k) for k in v]
608
+ v = list(v.values())
609
+ self.data[name] = doubles(v)
610
+
611
+ def _raw(self, name: str) -> None:
612
+ self.data[name] = self.args.get(name)
613
+
614
+ _cells = _raw