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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- 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
|