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
|
@@ -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
|