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/shapes.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Many rectangles or polygons in one call (vec_io's ``draw_rects`` and ``draw_polygons``)."""
|
|
2
|
+
|
|
3
|
+
from array import array
|
|
4
|
+
from typing import Any
|
|
5
|
+
from .canvas import Canvas
|
|
6
|
+
|
|
7
|
+
__all__ = ["draw_rects", "draw_polygons"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def draw_rects(canvas: Canvas, x: Any, y: Any, w: Any, h: Any, fill: Any = None,
|
|
11
|
+
stroke: Any = None, stroke_width: Any = None, simplify: Any = None) -> None:
|
|
12
|
+
"""Rectangles with top-left corners ``(x, y)`` and sizes ``(w, h)`` (page units);
|
|
13
|
+
``fill``, ``stroke`` and ``stroke_width``: one value or one per rectangle."""
|
|
14
|
+
if not len(x):
|
|
15
|
+
return
|
|
16
|
+
canvas.draw_rects(x, y, w, h, fill=fill, stroke=stroke, stroke_width=stroke_width,
|
|
17
|
+
simplify=simplify)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def draw_polygons(canvas: Canvas, polygons: list[list[tuple[Any, Any]]], fill: Any = None,
|
|
21
|
+
stroke: Any = None, stroke_width: Any = None, fill_rule: str = "nonzero",
|
|
22
|
+
simplify: Any = None) -> None:
|
|
23
|
+
"""Polygons, each a list of rings ``(xs, ys)`` filled together (holes work);
|
|
24
|
+
``fill``, ``stroke`` and ``stroke_width``: one value or one per polygon."""
|
|
25
|
+
if not polygons:
|
|
26
|
+
return
|
|
27
|
+
xs, ys = array("d"), array("d")
|
|
28
|
+
ring_starts, path_starts = array("q"), array("q")
|
|
29
|
+
for poly in polygons:
|
|
30
|
+
path_starts.append(len(ring_starts))
|
|
31
|
+
for rx, ry in poly:
|
|
32
|
+
ring_starts.append(len(xs))
|
|
33
|
+
xs.extend(rx)
|
|
34
|
+
ys.extend(ry)
|
|
35
|
+
ring_starts.append(len(xs))
|
|
36
|
+
path_starts.append(len(ring_starts) - 1)
|
|
37
|
+
canvas.draw_polygons((xs, ys, ring_starts, path_starts), fill=fill, stroke=stroke,
|
|
38
|
+
stroke_width=stroke_width, fill_rule=fill_rule, simplify=simplify)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fill_ring_sets(canvas: Canvas, ring_sets: list[tuple[Any, Any, Any]], fill: Any,
|
|
42
|
+
simplify: Any = None) -> None:
|
|
43
|
+
"""Paths given as vec_io ring arrays ``(xs, ys, ring_starts)`` (one path each), one fill
|
|
44
|
+
per path."""
|
|
45
|
+
xs, ys = array("d"), array("d")
|
|
46
|
+
ring_starts, path_starts = array("q"), array("q")
|
|
47
|
+
for rx, ry, starts in ring_sets:
|
|
48
|
+
path_starts.append(len(ring_starts))
|
|
49
|
+
base = len(xs)
|
|
50
|
+
ring_starts.extend(base + s for s in starts[:-1])
|
|
51
|
+
xs.extend(rx)
|
|
52
|
+
ys.extend(ry)
|
|
53
|
+
if not len(ring_starts):
|
|
54
|
+
return
|
|
55
|
+
ring_starts.append(len(xs))
|
|
56
|
+
path_starts.append(len(ring_starts) - 1)
|
|
57
|
+
canvas.draw_polygons((xs, ys, ring_starts, path_starts), fill=fill, fill_rule="nonzero",
|
|
58
|
+
simplify=simplify)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def swatch(x: float, y: float, w: float, h: float) -> tuple[float, float, float, float]:
|
|
62
|
+
"""The rectangle legend symbols of filled series use in a symbol box ``(x, y, w, h)``:
|
|
63
|
+
0.6em high with the default row height (the box is 0.8 of the row, 1.35em), as wide
|
|
64
|
+
as the box, like line symbols."""
|
|
65
|
+
sh = 0.6 / 1.08 * h
|
|
66
|
+
return x, y + (h - sh) / 2, w, sh
|
plot_io/sharing.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Shared axes: plots whose x (or y) axes form a group have one scale, one set of limits,
|
|
2
|
+
the same ticks and category order, and (``label_outer``) tick labels and titles only on
|
|
3
|
+
the outer plots of each column (x) or row (y).
|
|
4
|
+
|
|
5
|
+
Groups come from the figure's ``share_x``/``share_y``/``share`` (all plots, plots of a
|
|
6
|
+
column span, plots of a row span, or the plots holding a plot type: ``share_y="bigwig"``)
|
|
7
|
+
and from plots' own ``share_x=other`` (chained). The figure's ``share_color`` and
|
|
8
|
+
``share_size`` group plots the same way for their series' color and size axes
|
|
9
|
+
(:func:`plot_io.mapped.train`). The last plot of
|
|
10
|
+
a figure group (``figure.group``) shows the x tick labels too, under ``label_outer``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__all__ = ["check_figure_share", "check_legend_share", "check_share", "groups", "leader", "apply"]
|
|
17
|
+
|
|
18
|
+
_MODES = ("all", "col", "row")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check(v: Any, types: bool, grid: bool = False) -> Any:
|
|
22
|
+
"""A sharing mode: ``False``, ``True``/``"all"``, ``"col"``, ``"row"``; with ``types``
|
|
23
|
+
a plot type's name, with ``grid`` ``"grid"``."""
|
|
24
|
+
if v is False or v is True:
|
|
25
|
+
return "all" if v else False
|
|
26
|
+
s = v.strip() if isinstance(v, str) else None
|
|
27
|
+
if s is not None and (s.lower() in _MODES or (grid and s.lower() == "grid")):
|
|
28
|
+
return s.lower()
|
|
29
|
+
if s is not None and types:
|
|
30
|
+
from .layers import layer_types
|
|
31
|
+
if s in layer_types():
|
|
32
|
+
return s
|
|
33
|
+
kinds = "False, True, 'all', 'col', 'row'" + (", 'grid'" if grid else "")
|
|
34
|
+
tail = " or a plot type's name" if types else ""
|
|
35
|
+
raise ValueError(f"expected {kinds}{tail}, got {v!r}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_figure_share(v: Any) -> Any:
|
|
39
|
+
"""``False``, ``True``/``"all"``, ``"col"``, ``"row"`` or a plot type's name (the
|
|
40
|
+
plots holding one: ``"bigwig"``)."""
|
|
41
|
+
return _check(v, True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_legend_share(v: Any) -> Any:
|
|
45
|
+
"""The figure's ``share_legend``: ``False``, ``True``/``"all"``, ``"row"`` or ``"col"``."""
|
|
46
|
+
return _check(v, False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_share(v: Any) -> Any:
|
|
50
|
+
"""The figure's ``share``: as ``share_x`` or ``"grid"`` (x per column, y per row)."""
|
|
51
|
+
return _check(v, True, grid=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def figure_modes(fig: Any) -> dict[str, Any]:
|
|
55
|
+
"""The figure's sharing mode per axis role."""
|
|
56
|
+
share = fig.resolve("share")
|
|
57
|
+
out: dict[str, Any] = {"x": False, "y": False}
|
|
58
|
+
if share == "grid":
|
|
59
|
+
out = {"x": "col", "y": "row"}
|
|
60
|
+
elif share:
|
|
61
|
+
out = {"x": share, "y": share}
|
|
62
|
+
for role in ("x", "y"):
|
|
63
|
+
v = fig.resolve(f"share_{role}")
|
|
64
|
+
if v is not None:
|
|
65
|
+
out[role] = v
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _holds(plot: Any, kind: str) -> bool:
|
|
70
|
+
"""Whether a plot holds a series of the plot type named ``kind``."""
|
|
71
|
+
from .layers import layer_types
|
|
72
|
+
cls = layer_types().get(kind)
|
|
73
|
+
return isinstance(cls, type) and any(isinstance(s, cls) for s in plot.series)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _Union:
|
|
77
|
+
def __init__(self, items: list[Any]) -> None:
|
|
78
|
+
self.parent = {id(p): id(p) for p in items}
|
|
79
|
+
|
|
80
|
+
def find(self, k: int) -> int:
|
|
81
|
+
while self.parent[k] != k:
|
|
82
|
+
self.parent[k] = self.parent[self.parent[k]]
|
|
83
|
+
k = self.parent[k]
|
|
84
|
+
return k
|
|
85
|
+
|
|
86
|
+
def join(self, a: Any, b: Any) -> None:
|
|
87
|
+
ra, rb = self.find(id(a)), self.find(id(b))
|
|
88
|
+
if ra != rb:
|
|
89
|
+
self.parent[rb] = ra
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def groups(fig: Any, areas: list[Any], role: str) -> list[tuple[list[Any], bool]]:
|
|
93
|
+
"""Groups of areas sharing axis ``role`` (``"x"``, ``"y"``, or ``"color"``/``"size"``:
|
|
94
|
+
their series' color or size axes; ``"legend"``: one figure legend), two or more each,
|
|
95
|
+
with whether the group comes from the figure's setting (outer labels by default)."""
|
|
96
|
+
from .plot import Plot
|
|
97
|
+
|
|
98
|
+
mapped = role in ("color", "size", "legend")
|
|
99
|
+
plots = [a for a in areas if isinstance(a.obj, Plot) and (mapped or a.obj.resolve("axes"))]
|
|
100
|
+
if not plots:
|
|
101
|
+
return []
|
|
102
|
+
mode = fig.resolve(f"share_{role}") if mapped else figure_modes(fig)[role]
|
|
103
|
+
u = _Union(plots)
|
|
104
|
+
own = {id(a): None if mapped else a.obj.own_share(role) for a in plots}
|
|
105
|
+
figure_made: set[int] = set()
|
|
106
|
+
if mode:
|
|
107
|
+
free = [a for a in plots if own[id(a)] is None]
|
|
108
|
+
keys: dict[int, Any]
|
|
109
|
+
if mode == "all":
|
|
110
|
+
keys = {id(a): 0 for a in free}
|
|
111
|
+
elif mode == "col":
|
|
112
|
+
keys = {id(a): (a.c0, a.c1) for a in free}
|
|
113
|
+
elif mode == "row":
|
|
114
|
+
keys = {id(a): (a.r0, a.r1) for a in free}
|
|
115
|
+
else: # the plots holding a plot type
|
|
116
|
+
free = [a for a in free if _holds(a.obj, mode)]
|
|
117
|
+
keys = {id(a): 0 for a in free}
|
|
118
|
+
first: dict[Any, Any] = {}
|
|
119
|
+
for a in free:
|
|
120
|
+
k = keys[id(a)]
|
|
121
|
+
if k in first:
|
|
122
|
+
u.join(first[k], a)
|
|
123
|
+
else:
|
|
124
|
+
first[k] = a
|
|
125
|
+
figure_made = {id(a) for a in free}
|
|
126
|
+
by_plot = {id(a.obj): a for a in plots}
|
|
127
|
+
for a in plots:
|
|
128
|
+
other = own[id(a)]
|
|
129
|
+
if other is not None:
|
|
130
|
+
b = by_plot.get(id(other))
|
|
131
|
+
if b is None:
|
|
132
|
+
raise ValueError(f"{a.obj!r} shares its {role} axis with {other!r}, which is "
|
|
133
|
+
f"not a plot of the same figure")
|
|
134
|
+
u.join(b, a)
|
|
135
|
+
out: dict[int, list[Any]] = {}
|
|
136
|
+
for a in plots:
|
|
137
|
+
out.setdefault(u.find(id(a)), []).append(a)
|
|
138
|
+
result = []
|
|
139
|
+
for members in out.values():
|
|
140
|
+
if len(members) > 1:
|
|
141
|
+
result.append((members, all(id(m) in figure_made for m in members)))
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def leader(axes: list[Any], role: str,
|
|
146
|
+
given: Callable[[Any, str], bool] | None = None) -> int:
|
|
147
|
+
"""Index of the axis whose scale and limits a shared group takes: one with limits, else
|
|
148
|
+
one with a scale, else the first. Different limits or scales are an error. ``given(axis,
|
|
149
|
+
name)``: whether a setting is given (default: set on the axis)."""
|
|
150
|
+
is_given = given or (lambda a, name: a.is_set(name))
|
|
151
|
+
what = f"{role} axes" if role in ("color", "size") else f"{role} axis"
|
|
152
|
+
limited = [a for a in axes if a.resolve("limits") != "auto"]
|
|
153
|
+
specs = {repr(a.resolve("limits")) for a in limited}
|
|
154
|
+
if len(specs) > 1:
|
|
155
|
+
raise ValueError(f"plots sharing their {what} have different limits: "
|
|
156
|
+
f"{', '.join(sorted(specs))}")
|
|
157
|
+
scaled = [a for a in axes if is_given(a, "scale")]
|
|
158
|
+
scales = {a.resolve("scale") for a in scaled}
|
|
159
|
+
if len(scales) > 1:
|
|
160
|
+
raise ValueError(f"plots sharing their {what} have different scales: "
|
|
161
|
+
f"{', '.join(sorted(map(str, scales)))}")
|
|
162
|
+
for pick in (limited, scaled):
|
|
163
|
+
if pick:
|
|
164
|
+
return axes.index(pick[0])
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _leader(members: list[Any], role: str) -> Any:
|
|
169
|
+
return members[leader([getattr(m.obj, f"{role}_axis") for m in members], role)]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _check_times(members: list[Any], role: str) -> None:
|
|
173
|
+
"""Refuse a group of plots some of which draw dates on the axis and some numbers."""
|
|
174
|
+
kinds = set()
|
|
175
|
+
for m in members:
|
|
176
|
+
on = [s for s in m.state.role_items[role] if s.has_axis(role)]
|
|
177
|
+
if on and all(s.categories(role) is None for s in on):
|
|
178
|
+
kinds.add(any(getattr(s, "time_role", lambda r: None)(role) for s in on))
|
|
179
|
+
if len(kinds) > 1:
|
|
180
|
+
raise ValueError(f"plots sharing their {role} axis draw dates and numbers on it: a "
|
|
181
|
+
f"time axis would read the numbers as seconds")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def apply(fig: Any, areas: list[Any]) -> None:
|
|
185
|
+
"""Resolve every group: one scale and set of limits from all members' series (category
|
|
186
|
+
order in plot order), ticks shared, and outer labels."""
|
|
187
|
+
label_outer = fig.resolve("label_outer")
|
|
188
|
+
for role in ("x", "y"):
|
|
189
|
+
for members, from_figure in groups(fig, areas, role):
|
|
190
|
+
lead = _leader(members, role)
|
|
191
|
+
items = []
|
|
192
|
+
for m in members:
|
|
193
|
+
items.extend(m.state.role_items[role])
|
|
194
|
+
lstate = getattr(lead.state, role)
|
|
195
|
+
if lstate.axis.resolve("scale") is None:
|
|
196
|
+
_check_times(members, role)
|
|
197
|
+
try:
|
|
198
|
+
lstate.resolve(items, lead.state.padded)
|
|
199
|
+
except ValueError as e:
|
|
200
|
+
if len(members) < 2 or "mixes categorical and numeric" not in str(e):
|
|
201
|
+
raise
|
|
202
|
+
raise ValueError(f"plots sharing their {role} axis (share, share_{role}) draw "
|
|
203
|
+
f"categories on one and numbers on another: share only plots "
|
|
204
|
+
f"of one kind") from None
|
|
205
|
+
leaves = any(getattr(m.state, role).call_leaves for m in members)
|
|
206
|
+
for m in members:
|
|
207
|
+
st = getattr(m.state, role)
|
|
208
|
+
st.call_leaves = leaves # a dendrogram's leaves name them all
|
|
209
|
+
st.shared = lstate if m is not lead else None
|
|
210
|
+
if m is not lead:
|
|
211
|
+
st.copy_limits(lstate)
|
|
212
|
+
outer = label_outer if label_outer is not None else from_figure
|
|
213
|
+
if not outer:
|
|
214
|
+
continue
|
|
215
|
+
ends = {id(g.end()) for g in fig.groups} if role == "x" else set()
|
|
216
|
+
for m in members:
|
|
217
|
+
st = getattr(m.state, role)
|
|
218
|
+
bottom = _is_outer(m, members, role)
|
|
219
|
+
st.hide_labels = not bottom and id(m.obj) not in ends
|
|
220
|
+
st.hide_title = not bottom
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _is_outer(a: Any, members: list[Any], role: str) -> bool:
|
|
224
|
+
if role == "x":
|
|
225
|
+
same = [m for m in members if (m.c0, m.c1) == (a.c0, a.c1)]
|
|
226
|
+
if getattr(a.state, "x").on_far_side():
|
|
227
|
+
return a.r0 == min(m.r0 for m in same)
|
|
228
|
+
return a.r1 == max(m.r1 for m in same)
|
|
229
|
+
same = [m for m in members if (m.r0, m.r1) == (a.r0, a.r1)]
|
|
230
|
+
if getattr(a.state, "y").on_far_side():
|
|
231
|
+
return a.c1 == max(m.c1 for m in same)
|
|
232
|
+
return a.c0 == min(m.c0 for m in same)
|
plot_io/sizemaps.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Size maps: the range of marker diameters mapped values are drawn with (their limits and
|
|
2
|
+
scale are a size axis', :mod:`plot_io.mapped`)."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import kernels
|
|
9
|
+
from .settings import one_of
|
|
10
|
+
|
|
11
|
+
__all__ = ["SizeMap", "DEFAULT"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _size(v: Any) -> Any:
|
|
15
|
+
"""A marker diameter: px or any length (``"1.2em"``), resolved by the series using
|
|
16
|
+
the map."""
|
|
17
|
+
from .validators import length
|
|
18
|
+
return length(lo=0)(v)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _size_px(v: Any, owner: Any) -> float:
|
|
22
|
+
if isinstance(v, (int, float)) and (owner is None or v != v):
|
|
23
|
+
return float(v)
|
|
24
|
+
if owner is None:
|
|
25
|
+
raise ValueError(f"SizeMap size {v!r}: a relative length needs the series it is "
|
|
26
|
+
f"drawn for (owner=)")
|
|
27
|
+
from .units import resolve_raw
|
|
28
|
+
from .validators import length
|
|
29
|
+
return resolve_raw(owner, "size", length(lo=0), v)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SizeMap:
|
|
33
|
+
"""Marker diameters (px) over [0, 1]: the positions of values between their size axis'
|
|
34
|
+
limits.
|
|
35
|
+
|
|
36
|
+
``sizes``: the diameters at the two limits. ``by="area"`` moves the marker area
|
|
37
|
+
linearly between the end areas; ``by="diameter"`` moves the diameter. ``under`` and
|
|
38
|
+
``over``: sizes below and above the limits (default: the end sizes; ``"none"`` leaves
|
|
39
|
+
those points out); ``bad``: size of points with a missing value (default: left out).
|
|
40
|
+
|
|
41
|
+
With ``sizes=(0, d)``, ``by="area"`` and ``size_limits="0/max"``, a marker's area is
|
|
42
|
+
proportional to its value. ``size_map`` also takes a pair of diameters.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, sizes: Sequence[float] = (3.0, 13.0), by: str = "area",
|
|
46
|
+
under: Any = None, over: Any = None, bad: Any = None) -> None:
|
|
47
|
+
if len(sizes) != 2:
|
|
48
|
+
raise ValueError("sizes must be a pair (smallest, largest)")
|
|
49
|
+
self.sizes = (_size(sizes[0]), _size(sizes[1]))
|
|
50
|
+
self.by = one_of("area", "diameter")(by)
|
|
51
|
+
self.under = self._extreme(under)
|
|
52
|
+
self.over = self._extreme(over)
|
|
53
|
+
self.bad = self._extreme(bad)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _extreme(v: Any) -> Any:
|
|
57
|
+
if v is None:
|
|
58
|
+
return None
|
|
59
|
+
if isinstance(v, str) and v.lower() == "none":
|
|
60
|
+
return math.nan
|
|
61
|
+
return _size(v)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
from .units import given
|
|
65
|
+
return f"SizeMap({given(self.sizes[0])!r}–{given(self.sizes[1])!r}, by {self.by})"
|
|
66
|
+
|
|
67
|
+
def diameters(self, owner: Any = None) -> tuple[float, float]:
|
|
68
|
+
"""The end sizes in px, resolved for series ``owner`` (its `em`, scaling)."""
|
|
69
|
+
return _size_px(self.sizes[0], owner), _size_px(self.sizes[1], owner)
|
|
70
|
+
|
|
71
|
+
def map(self, values: Any, flo: float, fhi: float, function: str | None,
|
|
72
|
+
owner: Any = None) -> Any:
|
|
73
|
+
"""Diameters (px) for ``values``, with ``flo``/``fhi`` the limits after the scale
|
|
74
|
+
function; lengths resolved for series ``owner``."""
|
|
75
|
+
d0, d1 = self.diameters(owner)
|
|
76
|
+
span = fhi - flo
|
|
77
|
+
nan = math.nan
|
|
78
|
+
under = d0 if self.under is None else _size_px(self.under, owner)
|
|
79
|
+
over = d1 if self.over is None else _size_px(self.over, owner)
|
|
80
|
+
bad = nan if self.bad is None else _size_px(self.bad, owner)
|
|
81
|
+
if self.by == "diameter":
|
|
82
|
+
k = (d1 - d0) / span if span else 0.0
|
|
83
|
+
lo, hi = (d0, d1) if d1 >= d0 else (d1, d0)
|
|
84
|
+
u, o = (under, over) if d1 >= d0 else (over, under)
|
|
85
|
+
return kernels.scaled(values, k, d0 - k * flo, function, low=lo, high=hi,
|
|
86
|
+
under=u, over=o, nan=bad)
|
|
87
|
+
a0, a1 = d0 * d0, d1 * d1
|
|
88
|
+
k = (a1 - a0) / span if span else 0.0
|
|
89
|
+
lo, hi = (a0, a1) if a1 >= a0 else (a1, a0)
|
|
90
|
+
u, o = (under * under, over * over) if a1 >= a0 else (over * over, under * under)
|
|
91
|
+
areas = kernels.scaled(values, k, a0 - k * flo, function, low=lo, high=hi,
|
|
92
|
+
under=u, over=o, nan=bad * bad)
|
|
93
|
+
return kernels.scaled(areas, 1.0, 0.0, "sqrt")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#: The sizes of size axes without a ``size_map``.
|
|
97
|
+
DEFAULT = SizeMap()
|
plot_io/spec.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Renders: the model's settings resolved during one render.
|
|
2
|
+
|
|
3
|
+
A render (:func:`rendering`) resolves a setting of an object on its first plain read
|
|
4
|
+
(``obj.resolve(name)``) and keeps the value for the rest of the render: a dict per object
|
|
5
|
+
(:meth:`Render.lookup`). A setting never read is never resolved. The render also holds
|
|
6
|
+
what the renderer computes about the model: the layout boxes that ``%`` of the axes refers
|
|
7
|
+
to, the plot's colors given to series without one, and the trained color and size axes.
|
|
8
|
+
|
|
9
|
+
A setting whose value needs what only its use provides (``%`` of a layout box, of a slot,
|
|
10
|
+
an axis scale) is :data:`LAZY` in the render, and resolved where it is used, against the
|
|
11
|
+
render's boxes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import contextvars
|
|
16
|
+
import warnings
|
|
17
|
+
from collections.abc import Generator
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .units import NeedsContext
|
|
21
|
+
|
|
22
|
+
__all__ = ["LAZY", "NeedsBox", "Render", "rendering", "current", "set_boxes", "auto_colors",
|
|
23
|
+
"warn", "warnings_once"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Lazy:
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return "LAZY"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#: The value of a setting resolved where it is used (it needs a layout box, a slot, a
|
|
34
|
+
#: scale).
|
|
35
|
+
LAZY: Any = _Lazy()
|
|
36
|
+
_MISS = object()
|
|
37
|
+
#: The values of an object outside the render's figure: none kept.
|
|
38
|
+
_OUTSIDE: dict[str, Any] = {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NeedsBox(NeedsContext):
|
|
42
|
+
"""``%`` of a layout box, resolved before the box is known."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Render:
|
|
46
|
+
"""One render of a figure: the resolved settings of its objects, and what the renderer
|
|
47
|
+
computes about them (layout boxes, the plot's colors)."""
|
|
48
|
+
|
|
49
|
+
__slots__ = ("root", "theme", "values", "held", "tree", "boxes", "colors", "plots", "calls",
|
|
50
|
+
"placed", "widths", "scales", "size_keys", "runs", "levels",
|
|
51
|
+
"levels_group")
|
|
52
|
+
|
|
53
|
+
def __init__(self, root: Any) -> None:
|
|
54
|
+
self.root = root
|
|
55
|
+
self.theme = root._theme()
|
|
56
|
+
#: ``id(obj)`` → ``{name: value}``: the settings of ``obj`` read in this render.
|
|
57
|
+
self.values: dict[int, dict[str, Any]] = {}
|
|
58
|
+
#: the objects of ``values`` (kept alive, so that their ids stay theirs).
|
|
59
|
+
self.held: list[Any] = []
|
|
60
|
+
#: the ids of the objects of the figure (made on demand, see :meth:`changed`).
|
|
61
|
+
self.tree: set[int] | None = None
|
|
62
|
+
#: ``id(obj)`` → ``{"axes_width": px, …}``: the boxes `%` refers to.
|
|
63
|
+
self.boxes: dict[int, dict[str, float]] = {}
|
|
64
|
+
#: ``id(series)`` → the plot's colors it takes (``None``: it has its own).
|
|
65
|
+
self.colors: dict[int, list[Any] | None] = {}
|
|
66
|
+
#: level axes' colors (``(color group, axis key)`` → ``(plot id, {level: color})``)
|
|
67
|
+
#: and each plot's color group (``share_color``), see :mod:`plot_io.levels`
|
|
68
|
+
self.levels: dict[Any, Any] = {}
|
|
69
|
+
self.levels_group: dict[int, int] = {}
|
|
70
|
+
#: ``id(plot)`` → its calls (layers built, positions applied), ``id(series)`` → its
|
|
71
|
+
#: call (see :mod:`plot_io.layers.pipeline`).
|
|
72
|
+
self.plots: dict[int, list[Any]] = {}
|
|
73
|
+
self.calls: dict[int, Any] = {}
|
|
74
|
+
#: ``id(plot)`` → a drawing context (no canvas) of its axes as the last layout round
|
|
75
|
+
#: placed them: what stats binned at the axes' resolution read (hexagon lattices).
|
|
76
|
+
self.placed: dict[int, Any] = {}
|
|
77
|
+
#: ``id(plot)`` → the axes width (px) genome data were binned for.
|
|
78
|
+
self.widths: dict[int, float] = {}
|
|
79
|
+
#: ``id(axis)`` → the trained state of a color or size axis (see
|
|
80
|
+
#: :func:`plot_io.mapped.train`).
|
|
81
|
+
self.scales: dict[int, Any] = {}
|
|
82
|
+
#: the size keys of the trained size axes (see :func:`plot_io.mapped.size_keys`).
|
|
83
|
+
self.size_keys: dict[Any, Any] = {}
|
|
84
|
+
#: shaped lines of rich content by font database, font and content (see
|
|
85
|
+
#: :func:`plot_io.text.shape`; plain strings are memoized in the font database)
|
|
86
|
+
self.runs: dict[Any, Any] = {}
|
|
87
|
+
|
|
88
|
+
def lookup(self, obj: Any, name: str) -> Any:
|
|
89
|
+
"""The resolved value of ``obj``'s setting ``name`` (resolved on its first read in
|
|
90
|
+
the render), or :data:`LAZY` (resolved where used, or ``obj`` is not of the
|
|
91
|
+
render's figure). Markup in a text setting is checked when it is resolved."""
|
|
92
|
+
d = self.values.get(id(obj))
|
|
93
|
+
if d is None:
|
|
94
|
+
d = self.values[id(obj)] = {} if self._holds(obj) else _OUTSIDE
|
|
95
|
+
self.held.append(obj)
|
|
96
|
+
v = d.get(name, _MISS)
|
|
97
|
+
if v is _MISS:
|
|
98
|
+
if d is _OUTSIDE:
|
|
99
|
+
return LAZY
|
|
100
|
+
v = d[name] = self._compute(obj, name)
|
|
101
|
+
return v
|
|
102
|
+
|
|
103
|
+
def _holds(self, obj: Any) -> bool:
|
|
104
|
+
"""Whether ``obj`` is of the render's figure (not an object being made)."""
|
|
105
|
+
try:
|
|
106
|
+
return obj._root() is self.root
|
|
107
|
+
except AttributeError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def changed(self, obj: Any) -> None:
|
|
111
|
+
"""``obj`` was set during the render: when it is of the figure (not an object being
|
|
112
|
+
made by the render), drop what the render resolved and computed from the model."""
|
|
113
|
+
if id(obj) not in self.values:
|
|
114
|
+
if not self._holds(obj):
|
|
115
|
+
return
|
|
116
|
+
if self.tree is None:
|
|
117
|
+
self.tree = set()
|
|
118
|
+
todo = [self.root]
|
|
119
|
+
while todo:
|
|
120
|
+
o = todo.pop()
|
|
121
|
+
if id(o) not in self.tree:
|
|
122
|
+
self.tree.add(id(o))
|
|
123
|
+
todo.extend(o._children())
|
|
124
|
+
if id(obj) not in self.tree:
|
|
125
|
+
return
|
|
126
|
+
self.values.clear()
|
|
127
|
+
self.plots.clear()
|
|
128
|
+
self.calls.clear()
|
|
129
|
+
|
|
130
|
+
def _compute(self, obj: Any, name: str) -> Any:
|
|
131
|
+
# Kept values do not depend on the layout: the boxes are hidden while one is
|
|
132
|
+
# resolved, so that `%` of a box stays LAZY.
|
|
133
|
+
boxes, widths = self.boxes, self.widths
|
|
134
|
+
try:
|
|
135
|
+
if boxes or widths:
|
|
136
|
+
self.boxes, self.widths = {}, {}
|
|
137
|
+
try:
|
|
138
|
+
v = obj._resolve(name, theme=self.theme)
|
|
139
|
+
finally:
|
|
140
|
+
self.boxes, self.widths = boxes, widths
|
|
141
|
+
else:
|
|
142
|
+
v = obj._resolve(name, theme=self.theme)
|
|
143
|
+
except NeedsContext:
|
|
144
|
+
return LAZY
|
|
145
|
+
texts = _TEXTS.get(type(obj))
|
|
146
|
+
if texts is None:
|
|
147
|
+
texts = _texts(type(obj))
|
|
148
|
+
if name in texts and v is not None and self.lookup(obj, "markup"):
|
|
149
|
+
from .markup import check
|
|
150
|
+
try:
|
|
151
|
+
check(v)
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
raise ValueError(f"{obj.kind} setting {name!r}: {e}") from None
|
|
154
|
+
return v
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_TEXTS: dict[type, frozenset[str]] = {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _texts(cls: type) -> frozenset[str]:
|
|
161
|
+
"""The text settings of settings class ``cls`` whose markup is checked (none without
|
|
162
|
+
a ``markup`` setting)."""
|
|
163
|
+
t = _TEXTS.get(cls)
|
|
164
|
+
if t is None:
|
|
165
|
+
from .validators import text
|
|
166
|
+
names = cls.SETTINGS # type: ignore[attr-defined]
|
|
167
|
+
t = _TEXTS[cls] = frozenset(n for n, s in names.items() if s.check is text) \
|
|
168
|
+
if "markup" in names else frozenset()
|
|
169
|
+
return t
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
_CURRENT: contextvars.ContextVar[Render | None] = contextvars.ContextVar("plot_io_render",
|
|
173
|
+
default=None)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def current() -> Render | None:
|
|
177
|
+
"""The render in progress, if any."""
|
|
178
|
+
return _CURRENT.get()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@contextlib.contextmanager
|
|
182
|
+
def rendering(root: Any) -> Generator[Render]:
|
|
183
|
+
"""A render of ``root`` (a figure). Inside a render of the same figure, that
|
|
184
|
+
render."""
|
|
185
|
+
r = _CURRENT.get()
|
|
186
|
+
if r is not None and r.root is root:
|
|
187
|
+
yield r
|
|
188
|
+
return
|
|
189
|
+
r = Render(root)
|
|
190
|
+
token = _CURRENT.set(r)
|
|
191
|
+
try:
|
|
192
|
+
yield r
|
|
193
|
+
finally:
|
|
194
|
+
_CURRENT.reset(token)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_PENDING: contextvars.ContextVar[dict[Any, tuple[str, type[Warning]]] | None] = \
|
|
198
|
+
contextvars.ContextVar("plot_io_warnings", default=None)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def warn(message: str, key: Any = None, category: type[Warning] = UserWarning,
|
|
202
|
+
stacklevel: int = 2) -> None:
|
|
203
|
+
"""Warn once per layout (:func:`warnings_once`): a warning given again with the same
|
|
204
|
+
``key`` (default: the message) replaces the earlier one, so a layout round's later
|
|
205
|
+
numbers are the ones shown. Outside a layout, warn at once."""
|
|
206
|
+
pending = _PENDING.get()
|
|
207
|
+
if pending is None:
|
|
208
|
+
warnings.warn(message, category, stacklevel=stacklevel + 1)
|
|
209
|
+
return
|
|
210
|
+
pending[message if key is None else key] = (message, category)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@contextlib.contextmanager
|
|
214
|
+
def warnings_once() -> Generator[None]:
|
|
215
|
+
"""Collect :func:`warn` calls and give each once at the end (nested: the outermost
|
|
216
|
+
gives them)."""
|
|
217
|
+
if _PENDING.get() is not None:
|
|
218
|
+
yield
|
|
219
|
+
return
|
|
220
|
+
pending: dict[Any, tuple[str, type[Warning]]] = {}
|
|
221
|
+
token = _PENDING.set(pending)
|
|
222
|
+
try:
|
|
223
|
+
yield
|
|
224
|
+
finally:
|
|
225
|
+
_PENDING.reset(token)
|
|
226
|
+
for message, category in pending.values():
|
|
227
|
+
warnings.warn(message, category, stacklevel=4)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def set_boxes(obj: Any, **boxes: float) -> None:
|
|
231
|
+
"""The layout boxes `%` of ``obj``'s settings (and its children's) refers to."""
|
|
232
|
+
r = _CURRENT.get()
|
|
233
|
+
if r is not None:
|
|
234
|
+
r.boxes[id(obj)] = boxes
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def auto_colors(series: Any) -> list[Any] | None:
|
|
238
|
+
"""The plot's colors ``series`` takes in the render in progress (``None``: none)."""
|
|
239
|
+
r = _CURRENT.get()
|
|
240
|
+
return None if r is None else r.colors.get(id(series))
|