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