plot-io 0.12.0__py3-none-win_arm64.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-arm64.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,87 @@
|
|
|
1
|
+
"""The drawing context: the canvas, the axes rectangle, the mappers, the plot's trained
|
|
2
|
+
colors, and the x↔y swap of horizontal layers (trained color and size axes are in the render,
|
|
3
|
+
:func:`plot_io.mapped.trained`)."""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from typing import Any
|
|
7
|
+
from ..canvas import Canvas
|
|
8
|
+
|
|
9
|
+
__all__ = ["DrawContext", "Frame"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Frame:
|
|
13
|
+
"""The mappers of a layer's frame: ``u`` for its x, ``v`` for its y; ``flip``: its x is
|
|
14
|
+
the plot's y (horizontal layers)."""
|
|
15
|
+
|
|
16
|
+
__slots__ = ("u", "v", "flip")
|
|
17
|
+
|
|
18
|
+
def __init__(self, u: Any, v: Any, flip: bool) -> None:
|
|
19
|
+
self.u, self.v, self.flip = u, v, flip
|
|
20
|
+
|
|
21
|
+
def page(self, a: Any, b: Any) -> tuple[Any, Any]:
|
|
22
|
+
"""Page ``(x, y)`` of frame page coordinates ``(a, b)`` (along u, along v)."""
|
|
23
|
+
return (b, a) if self.flip else (a, b)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DrawContext:
|
|
27
|
+
"""What a layer needs while drawing: the canvas (``None`` while laying out), the axes
|
|
28
|
+
rectangle and mappers, and the plot's colors (``cycle``, which ``"C<n>"`` refer to)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, canvas: Canvas | None, plot: Any, rect: tuple[float, float, float, float],
|
|
31
|
+
x: Any, y: Any, cycle: tuple) -> None:
|
|
32
|
+
self.canvas = canvas
|
|
33
|
+
self.plot = plot
|
|
34
|
+
self.rect = rect
|
|
35
|
+
self.x = x
|
|
36
|
+
self.y = y
|
|
37
|
+
x0, y0, x1, y1 = rect
|
|
38
|
+
self.xmap = x.mapper(x0, x1)
|
|
39
|
+
self.ymap = y.mapper(y1, y0)
|
|
40
|
+
self.cycle = cycle
|
|
41
|
+
self.figure_size = (x1, y1)
|
|
42
|
+
self.root: "DrawContext" = self # the plot's context (see for_call)
|
|
43
|
+
#: page boxes ``(x0, y0, x1, y1)`` point labels keep clear of (an inside legend)
|
|
44
|
+
self.avoid: list[tuple[float, float, float, float]] = []
|
|
45
|
+
|
|
46
|
+
def release(self) -> None:
|
|
47
|
+
"""Drops the plot's caches (styles and lazy columns, with their per-point buffers)
|
|
48
|
+
once the plot is drawn, before the document is written."""
|
|
49
|
+
for k in ("_memo", "_lazy"):
|
|
50
|
+
self.__dict__.pop(k, None)
|
|
51
|
+
|
|
52
|
+
def color(self, value: Any) -> Any:
|
|
53
|
+
from ..color.values import resolve_color
|
|
54
|
+
from ..units import Fixed
|
|
55
|
+
if isinstance(value, Fixed): # a named color kept as given
|
|
56
|
+
value = value.in_context(self.plot)
|
|
57
|
+
return resolve_color(value, self.cycle)
|
|
58
|
+
|
|
59
|
+
def for_call(self, call: Any) -> "DrawContext":
|
|
60
|
+
"""This context with mappers that apply the call's ``x_origin``/``x_scale``/
|
|
61
|
+
``x_extent`` (and ``y_…``), or with the y mapper of its lane (``call.lane``);
|
|
62
|
+
itself when it has none. ``root`` is the plot's context."""
|
|
63
|
+
from ..transform import affine
|
|
64
|
+
tx, ty = affine(call, "x"), affine(call, "y")
|
|
65
|
+
lane = getattr(call, "lane", None)
|
|
66
|
+
if tx is None and ty is None and lane is None:
|
|
67
|
+
return self
|
|
68
|
+
root = self.root
|
|
69
|
+
c = copy.copy(root)
|
|
70
|
+
if tx is not None:
|
|
71
|
+
c.xmap = root.xmap.affine(*tx, "x")
|
|
72
|
+
if lane is not None:
|
|
73
|
+
c.ymap = lane(root)
|
|
74
|
+
elif ty is not None:
|
|
75
|
+
c.ymap = root.ymap.affine(*ty, "y")
|
|
76
|
+
return c
|
|
77
|
+
|
|
78
|
+
def frame(self, layer: Any) -> Frame:
|
|
79
|
+
"""The mappers of ``layer``'s frame (swapped for a flipped layer)."""
|
|
80
|
+
if layer.flip:
|
|
81
|
+
return Frame(self.ymap, self.xmap, True)
|
|
82
|
+
return Frame(self.xmap, self.ymap, False)
|
|
83
|
+
|
|
84
|
+
def state(self, frame_role: str, layer: Any) -> Any:
|
|
85
|
+
"""The axis state of a layer's frame axis (``"x"``: along its u)."""
|
|
86
|
+
role = layer.frame_role(frame_role)
|
|
87
|
+
return self.x if role == "x" else self.y
|
plot_io/layers/core.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Layers and calls: what a plot type's call becomes before it is drawn.
|
|
2
|
+
|
|
3
|
+
A **call** (``p.scatter(…)``, ``plt.Box(…)``) is a model object, a :class:`~plot_io.layers.Series`
|
|
4
|
+
holding data and settings. Each render turns it into a :class:`Call`: its **layers**, one
|
|
5
|
+
per geom it draws, its legend entries, and what the axes and maps need from it.
|
|
6
|
+
|
|
7
|
+
A **layer** draws one geom from a table:
|
|
8
|
+
|
|
9
|
+
* the table holds the output of the call's stat (:mod:`~plot_io.layers.stats`), columns
|
|
10
|
+
in data units (shared by the layers of one call that draw the same output: a density's
|
|
11
|
+
fill and its line); a layer's ``stat`` and ``position`` name what made and places it,
|
|
12
|
+
as labels (its ``repr``), and do not run anything;
|
|
13
|
+
* the layer's ``aes`` maps the geom's channels to columns of the table;
|
|
14
|
+
* positions (``stack``, ``dodge``, ``jitter``, ``swarm``, ``pack``), requested on the
|
|
15
|
+
tables, add or replace columns once per plot over all its layers
|
|
16
|
+
(:mod:`~plot_io.layers.positions`);
|
|
17
|
+
* ``paint`` gives the per-item aesthetics (colors, widths, markers), resolved once the
|
|
18
|
+
the plot's colors and the color maps are trained;
|
|
19
|
+
* ``flip`` swaps x and y for the geom: a horizontal layer is a vertical one drawn with
|
|
20
|
+
its frame's x on the plot's y axis.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
from array import array
|
|
25
|
+
from typing import Any, Callable
|
|
26
|
+
|
|
27
|
+
from vec_io import Path
|
|
28
|
+
|
|
29
|
+
from .. import kernels
|
|
30
|
+
|
|
31
|
+
__all__ = ["Table", "Layer", "Call", "Entry", "ROLES", "KEY_GLYPH", "part_values",
|
|
32
|
+
"draw_part"]
|
|
33
|
+
|
|
34
|
+
ROLES = ("x", "y")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Table(dict):
|
|
38
|
+
"""Columns of one stat output (names → arrays or lists). Columns whose values need
|
|
39
|
+
the placed axes (slot widths in px, swarm offsets) are functions of the drawing
|
|
40
|
+
context in ``lazy``; :meth:`Layer.columns` resolves them."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args: Any, **kw: Any) -> None:
|
|
43
|
+
super().__init__(*args, **kw)
|
|
44
|
+
self.lazy: dict[str, Callable[[Any], Any]] = {}
|
|
45
|
+
#: position requests (see :mod:`~plot_io.layers.positions`): ``(group,
|
|
46
|
+
#: normalize)`` to stack, a ``Dodge`` to share category slots
|
|
47
|
+
self.stack: Any = None
|
|
48
|
+
self.dodge: Any = None
|
|
49
|
+
#: ``id`` of the table a resolved copy was made from (see :meth:`Layer.columns`)
|
|
50
|
+
self.origin = id(self)
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"Table({', '.join(self)})"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Layer:
|
|
57
|
+
"""One geom drawing columns of a table (see the module docstring)."""
|
|
58
|
+
|
|
59
|
+
__slots__ = ("geom", "table", "aes", "paint", "flip", "stat", "position", "overlay",
|
|
60
|
+
"occupy", "key", "options")
|
|
61
|
+
|
|
62
|
+
def __init__(self, geom: Any, table: Table, aes: dict[str, str] | None = None,
|
|
63
|
+
paint: Any = None, flip: bool = False, stat: str = "identity",
|
|
64
|
+
position: str = "identity", overlay: bool = False, occupy: Any = True,
|
|
65
|
+
key: bool = True, **options: Any) -> None:
|
|
66
|
+
self.geom = geom
|
|
67
|
+
self.table = table
|
|
68
|
+
self.aes = aes or {}
|
|
69
|
+
self.paint = paint
|
|
70
|
+
self.flip = flip
|
|
71
|
+
self.stat = stat
|
|
72
|
+
self.position = position
|
|
73
|
+
#: drawn after every layer of the plot, outside the axes clip (value labels)
|
|
74
|
+
self.overlay = overlay
|
|
75
|
+
#: legend placement: ``True`` (the geom's cells), ``False`` or ``"all"`` (the axes)
|
|
76
|
+
self.occupy = occupy
|
|
77
|
+
#: whether the call's legend symbol shows this layer
|
|
78
|
+
self.key = key
|
|
79
|
+
self.options = options
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return (f"Layer({self.stat} → {self.position} → {type(self.geom).__name__}"
|
|
83
|
+
f"{', flipped' if self.flip else ''})")
|
|
84
|
+
|
|
85
|
+
def col(self, channel: str) -> Any:
|
|
86
|
+
"""The column a channel of the geom reads (``None`` when the table has none)."""
|
|
87
|
+
return self.table.get(self.aes.get(channel, channel))
|
|
88
|
+
|
|
89
|
+
def has(self, channel: str) -> bool:
|
|
90
|
+
return self.aes.get(channel, channel) in self.table
|
|
91
|
+
|
|
92
|
+
def columns(self, ctx: Any) -> "Layer":
|
|
93
|
+
"""This layer with its lazy columns resolved for ``ctx`` (itself if it has none)."""
|
|
94
|
+
lazy = self.table.lazy
|
|
95
|
+
if not lazy:
|
|
96
|
+
return self
|
|
97
|
+
cache = ctx.root.__dict__.setdefault("_lazy", {})
|
|
98
|
+
key = id(self.table)
|
|
99
|
+
t = cache.get(key)
|
|
100
|
+
if t is None:
|
|
101
|
+
t = Table(self.table)
|
|
102
|
+
t.origin = self.table.origin
|
|
103
|
+
for name, fn in lazy.items():
|
|
104
|
+
t[name] = fn(ctx)
|
|
105
|
+
cache[key] = t
|
|
106
|
+
out = Layer.__new__(Layer)
|
|
107
|
+
for k in Layer.__slots__:
|
|
108
|
+
setattr(out, k, getattr(self, k))
|
|
109
|
+
out.table = t
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
def frame_role(self, role: str) -> str:
|
|
113
|
+
"""The geom's axis drawn on the plot's axis ``role``."""
|
|
114
|
+
if not self.flip:
|
|
115
|
+
return role
|
|
116
|
+
return "y" if role == "x" else "x"
|
|
117
|
+
|
|
118
|
+
def style(self, ctx: Any) -> Any:
|
|
119
|
+
"""The resolved aesthetics: of a :class:`~plot_io.layers.paint.Paint` (a role), or
|
|
120
|
+
``paint`` itself (fixed values)."""
|
|
121
|
+
p = self.paint
|
|
122
|
+
resolve = getattr(p, "resolve", None)
|
|
123
|
+
return resolve(ctx, False) if resolve is not None else (p or {})
|
|
124
|
+
|
|
125
|
+
def part(self) -> tuple[str, Any] | None:
|
|
126
|
+
"""This layer's part of a legend symbol: ``(kind, options)`` (``None``: none)."""
|
|
127
|
+
return self.geom.part(self)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Legend symbols as data
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
#: Whether a symbol part's values come from the paint resolved for the legend
|
|
135
|
+
#: (``glyph=True``) or as drawn on the axes.
|
|
136
|
+
KEY_GLYPH = {"marker": True, "line": False, "band": False, "rect": True, "polygon": True,
|
|
137
|
+
"rule": True, "gradient": True}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def part_values(kind: str, p: Any) -> Any:
|
|
141
|
+
"""The values a symbol part draws, from its layer's resolved paint ``p`` (``None``:
|
|
142
|
+
nothing drawn)."""
|
|
143
|
+
if kind == "marker":
|
|
144
|
+
return p.key_values() if p else None
|
|
145
|
+
p = p or {}
|
|
146
|
+
if kind == "line":
|
|
147
|
+
c, lw = p.get("color"), p.get("width")
|
|
148
|
+
if c is None or not lw:
|
|
149
|
+
return None
|
|
150
|
+
return (c, lw, p.get("dash"), p.get("opacity"))
|
|
151
|
+
if kind == "band":
|
|
152
|
+
return (p.get("color"), p.get("opacity", 1.0), p.get("edge"), p.get("edge_width"))
|
|
153
|
+
if kind in ("rect", "polygon"):
|
|
154
|
+
return (p.get("fill"), p.get("stroke"), p.get("line_width"))
|
|
155
|
+
if kind == "rule":
|
|
156
|
+
c = p.get("color")
|
|
157
|
+
return None if c is None else (c, p.get("width"))
|
|
158
|
+
if kind == "gradient":
|
|
159
|
+
from ..colorbar import gradient_stops
|
|
160
|
+
m = p.get("map")
|
|
161
|
+
return None if m is None else tuple(gradient_stops(m, 16))
|
|
162
|
+
raise ValueError(f"unknown symbol part {kind!r}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def draw_part(ctx: Any, kind: str, options: Any, v: Any, x: float, y: float, w: float,
|
|
166
|
+
h: float) -> None:
|
|
167
|
+
"""Draw one symbol part (values ``v`` of :func:`part_values`) in box ``x, y, w, h``."""
|
|
168
|
+
if v is None:
|
|
169
|
+
return
|
|
170
|
+
from ..color.values import with_alpha
|
|
171
|
+
from ..shapes import swatch
|
|
172
|
+
canvas = ctx.canvas
|
|
173
|
+
o = dict(options)
|
|
174
|
+
if kind == "marker":
|
|
175
|
+
marker, size, fill, stroke, lw, opacity = v
|
|
176
|
+
canvas.draw_markers([x + w / 2], [y + h * o.get("at", 0.5)], marker=marker,
|
|
177
|
+
size=min(size, h * o.get("max", 1.1)), fill=fill, stroke=stroke,
|
|
178
|
+
stroke_width=lw, opacity=opacity)
|
|
179
|
+
elif kind == "line":
|
|
180
|
+
c, lw, dash, opacity = v
|
|
181
|
+
if o.get("top"):
|
|
182
|
+
x, y, w, h = swatch(x, y, w, h)
|
|
183
|
+
cy = y
|
|
184
|
+
else:
|
|
185
|
+
cy = y + h / 2
|
|
186
|
+
kw: dict[str, Any] = {}
|
|
187
|
+
if dash is not None:
|
|
188
|
+
kw["stroke_dasharray"] = dash
|
|
189
|
+
if opacity is not None:
|
|
190
|
+
kw["opacity"] = opacity
|
|
191
|
+
canvas.draw_lines([x, x + w], [cy, cy], stroke=c, stroke_width=lw, **kw)
|
|
192
|
+
elif kind == "band":
|
|
193
|
+
c, opacity, edge, ew = v
|
|
194
|
+
if o.get("form") == "band":
|
|
195
|
+
if c is not None:
|
|
196
|
+
canvas.fill = with_alpha(c, opacity)
|
|
197
|
+
canvas.draw_rect(x, y + h * 0.2, w, h * 0.6, stroke=None)
|
|
198
|
+
return
|
|
199
|
+
x, y, w, h = swatch(x, y, w, h)
|
|
200
|
+
if c is not None:
|
|
201
|
+
canvas.fill = with_alpha(c, opacity)
|
|
202
|
+
canvas.draw_rect(x, y, w, h, stroke=None)
|
|
203
|
+
if edge is not None and ew:
|
|
204
|
+
canvas.stroke = edge
|
|
205
|
+
canvas.stroke_width = ew
|
|
206
|
+
canvas.draw_rect(x, y, w, h, fill=None)
|
|
207
|
+
elif kind in ("rect", "polygon"):
|
|
208
|
+
fill, stroke, lw = v
|
|
209
|
+
form = o.get("form")
|
|
210
|
+
if form == "hexagon":
|
|
211
|
+
canvas.draw_markers([x + w / 2], [y + h / 2], marker="M0.5 -0.29 L0.5 0.29 L0 "
|
|
212
|
+
"0.58 L-0.5 0.29 L-0.5 -0.29 L0 -0.58 Z", size=h * 0.9,
|
|
213
|
+
fill=fill, stroke=None)
|
|
214
|
+
return
|
|
215
|
+
if form == "fill":
|
|
216
|
+
if fill is not None:
|
|
217
|
+
canvas.fill = fill
|
|
218
|
+
canvas.draw_rect(*swatch(x, y, w, h), stroke=None)
|
|
219
|
+
return
|
|
220
|
+
path = Path().rect(*swatch(x, y, w, h))
|
|
221
|
+
if fill is not None:
|
|
222
|
+
canvas.draw_path(path, fill=fill, stroke=None)
|
|
223
|
+
if stroke is not None and (lw is not None if kind == "rect" else lw):
|
|
224
|
+
canvas.stroke_width = lw
|
|
225
|
+
canvas.draw_path(path, fill=None, stroke=stroke)
|
|
226
|
+
elif kind == "gradient":
|
|
227
|
+
from vec_io import LinearGradient
|
|
228
|
+
sx, sy, sw, sh = swatch(x, y, w, h)
|
|
229
|
+
canvas.draw_rect(sx, sy, sw, sh, fill=LinearGradient(sx, 0, sx + sw, 0, list(v)), stroke=None)
|
|
230
|
+
elif kind == "rule":
|
|
231
|
+
c, lw = v
|
|
232
|
+
cx = x + w / 2
|
|
233
|
+
if o.get("form") == "stem":
|
|
234
|
+
canvas.draw_lines([cx, cx], [y + h, y + h * 0.35], stroke=c, stroke_width=lw)
|
|
235
|
+
return
|
|
236
|
+
canvas.draw_lines([cx, cx], [y, y + h], stroke=c, stroke_width=lw)
|
|
237
|
+
canvas.draw_segments([cx - 3, cx - 3], [y, y + h], [cx + 3, cx + 3], [y, y + h],
|
|
238
|
+
stroke=c, stroke_width=lw)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Entry:
|
|
242
|
+
"""A legend entry of a call: its label and symbol, as data.
|
|
243
|
+
|
|
244
|
+
``symbol``: the parts of the call's key layers (``(kind, options)`` each, see
|
|
245
|
+
:func:`draw_part`); ``paint``: the paint of each part's layer, resolved to values once
|
|
246
|
+
per drawing context (:meth:`values`); ``colors``: a paint giving ``(fill, stroke)`` for
|
|
247
|
+
the legend's simple symbols. Two entries draw the same symbol when their symbols and
|
|
248
|
+
values are equal."""
|
|
249
|
+
|
|
250
|
+
__slots__ = ("label", "symbol", "paint", "colors")
|
|
251
|
+
|
|
252
|
+
def __init__(self, label: Any, symbol: Any, paint: Any, colors: Any = None) -> None:
|
|
253
|
+
self.label = label
|
|
254
|
+
self.symbol = symbol
|
|
255
|
+
self.paint = paint
|
|
256
|
+
self.colors = colors
|
|
257
|
+
|
|
258
|
+
def __repr__(self) -> str:
|
|
259
|
+
return f"Entry({self.label!r})"
|
|
260
|
+
|
|
261
|
+
def values(self, ctx: Any) -> tuple[Any, ...]:
|
|
262
|
+
"""The values each part draws in ``ctx``."""
|
|
263
|
+
cache = ctx.root.__dict__.setdefault("_entry_values", {})
|
|
264
|
+
got = cache.get(id(self))
|
|
265
|
+
if got is None:
|
|
266
|
+
got = cache[id(self)] = tuple(
|
|
267
|
+
part_values(kind, p.resolve(ctx, KEY_GLYPH[kind]))
|
|
268
|
+
for (kind, _), p in zip(self.symbol, self.paint))
|
|
269
|
+
return got
|
|
270
|
+
|
|
271
|
+
def key(self, ctx: Any) -> Any:
|
|
272
|
+
"""What the symbol draws, for comparing entries."""
|
|
273
|
+
return self.symbol, self.values(ctx)
|
|
274
|
+
|
|
275
|
+
# the legend's protocol (decorations have it too)
|
|
276
|
+
|
|
277
|
+
def legend_key_at(self, ctx: Any, i: int) -> Any:
|
|
278
|
+
return self.key(ctx)
|
|
279
|
+
|
|
280
|
+
def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float,
|
|
281
|
+
h: float) -> None:
|
|
282
|
+
for (kind, options), v in zip(self.symbol, self.values(ctx)):
|
|
283
|
+
draw_part(ctx, kind, options, v, x, y, w, h)
|
|
284
|
+
|
|
285
|
+
def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
|
|
286
|
+
return self.colors.resolve(ctx, True)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _no_room(fonts: Any, role: str) -> list[tuple[float, float, int]]:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class Call:
|
|
294
|
+
"""A call of a plot type in a render: its layers and what the axes, the color and size
|
|
295
|
+
maps, the plot's colors and the legend need from it."""
|
|
296
|
+
|
|
297
|
+
def __init__(self, series: Any) -> None:
|
|
298
|
+
self.series = series
|
|
299
|
+
self.layers: list[Layer] = []
|
|
300
|
+
#: tables positioned but not drawn as such (histogram sets merged into one layer)
|
|
301
|
+
self.tables: list[Table] = []
|
|
302
|
+
self.entries: list[Entry] = []
|
|
303
|
+
#: the plot's colors taken when the call has no color of its own (0: none)
|
|
304
|
+
self.color_count = 0
|
|
305
|
+
#: the colors of a level call (:func:`plot_io.levels.assign`; ``None``: ``color_count``)
|
|
306
|
+
self.level: Any = None
|
|
307
|
+
#: ``(axis, values)`` pairs mapped on color and size axes (``values`` may be a
|
|
308
|
+
#: function, called at training: ``None`` until the axes are placed)
|
|
309
|
+
self.color_sources: list[tuple[Any, Any]] = []
|
|
310
|
+
self.size_sources: list[tuple[Any, Any]] = []
|
|
311
|
+
#: whether the call is drawn on axes (pies are not)
|
|
312
|
+
self.axes = True
|
|
313
|
+
#: per plot role: categories (categorical data), values included in the extent
|
|
314
|
+
#: beyond the layers' columns, an extent replacing the layers' (``f(scale)``),
|
|
315
|
+
#: values automatic padding does not pass, the column name of the axis title
|
|
316
|
+
self.cats: dict[str, list[Any]] = {}
|
|
317
|
+
self.extra: dict[str, Any] = {}
|
|
318
|
+
self.extent_of: dict[str, Callable[[Any], tuple[int, float, float]]] = {}
|
|
319
|
+
self.sticky_values: dict[str, list[float]] = {}
|
|
320
|
+
self.titles: dict[str, Any] = {}
|
|
321
|
+
#: plot roles that include 0 under ``include_zero="auto"``, that take no
|
|
322
|
+
#: automatic padding, inverted by default; ``role → tz`` of date data
|
|
323
|
+
self.zero: set[str] = set()
|
|
324
|
+
self.exact: set[str] = set()
|
|
325
|
+
self.invert: set[str] = set()
|
|
326
|
+
#: plot roles whose ticks are integers (rows and columns of a grid)
|
|
327
|
+
self.whole: set[str] = set()
|
|
328
|
+
self.times: dict[str, Any] = {}
|
|
329
|
+
#: per plot role: the scale the call asks of an axis without one (``"genome"``),
|
|
330
|
+
#: ticks it gives (``[(value, label)]``: named rows; the axis takes them when every
|
|
331
|
+
#: call on it gives some), roles whose axis it hides (drawn when any call does not)
|
|
332
|
+
self.scales: dict[str, str] = {}
|
|
333
|
+
self.ticks: dict[str, list[tuple[float, Any]]] = {}
|
|
334
|
+
#: plot roles whose given tick labels are left out where they overlap the last kept
|
|
335
|
+
self.thin: set[str] = set()
|
|
336
|
+
#: plot roles whose tick labels are leaf names (dendrograms): all drawn, rotated
|
|
337
|
+
#: and then smaller before any is left out (the axis' ``label_min_size``)
|
|
338
|
+
self.leaves: set[str] = set()
|
|
339
|
+
self.hide: set[str] = set()
|
|
340
|
+
#: the genome region the call reads (genome series), and per plot role where it
|
|
341
|
+
#: differs (a square Hi-C matrix: its ``region2`` on y)
|
|
342
|
+
self.region: Any = None
|
|
343
|
+
self.regions: dict[str, Any] = {}
|
|
344
|
+
#: ``lane(ctx) -> Mapper``: the y mapper of a call drawn in a lane of its own
|
|
345
|
+
#: (interval rows, :mod:`plot_io.genome.lanes`), from the plot's context
|
|
346
|
+
self.lane: Callable[[Any], Any] | None = None
|
|
347
|
+
#: ``room(fonts, role)``: value labels drawn past their values (bars)
|
|
348
|
+
self.room: Callable[[Any, str], list[tuple[float, float, int]]] = _no_room
|
|
349
|
+
#: ``overhang(fonts, w, h) -> (left, right)``: labels drawn past the axes (pies)
|
|
350
|
+
self.overhang: Callable[..., tuple[float, float]] | None = None
|
|
351
|
+
#: ``resolution(ctx) -> key``: for stats binned at the placed axes' resolution
|
|
352
|
+
#: (hexagon lattices), the key of the binning for the axes of a drawing context;
|
|
353
|
+
#: when it changes between layout rounds, the stats and the color axes they map
|
|
354
|
+
#: are computed again (see :mod:`plot_io.layout`)
|
|
355
|
+
self.resolution: Callable[[Any], Any] | None = None
|
|
356
|
+
#: steps of the preset that need the plot's positions (layers built from stacked
|
|
357
|
+
#: values), run after them
|
|
358
|
+
self.after: list[Callable[[], None]] = []
|
|
359
|
+
|
|
360
|
+
def __repr__(self) -> str:
|
|
361
|
+
return f"Call({self.series!r}, {len(self.layers)} layers)"
|
|
362
|
+
|
|
363
|
+
# -- what the axes need --------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def has_axis(self, role: str) -> bool:
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
def categories(self, role: str) -> list[Any] | None:
|
|
369
|
+
return self.cats.get(role)
|
|
370
|
+
|
|
371
|
+
def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
|
|
372
|
+
"""``(count, low, high)`` of the call's values on axis ``role``, through the axis
|
|
373
|
+
scale's function: the union of its layers' geom extents and its extra values."""
|
|
374
|
+
fn = self.extent_of.get(role)
|
|
375
|
+
if fn is not None:
|
|
376
|
+
return fn(scale)
|
|
377
|
+
count, lo, hi = 0, math.inf, -math.inf
|
|
378
|
+
dropped = 0
|
|
379
|
+
for layer in self.layers:
|
|
380
|
+
if not layer.options.get("bounds", True):
|
|
381
|
+
continue
|
|
382
|
+
n, a, b = layer.geom.extent(layer, layer.frame_role(role), scale)
|
|
383
|
+
if n:
|
|
384
|
+
count += n
|
|
385
|
+
lo, hi = min(lo, a), max(hi, b)
|
|
386
|
+
if scale.base is not None:
|
|
387
|
+
dropped += layer.geom.dropped(layer, layer.frame_role(role), scale)
|
|
388
|
+
if dropped:
|
|
389
|
+
from ..spec import warn
|
|
390
|
+
kind = getattr(self.series, "kind", "series")
|
|
391
|
+
warn(f"{kind}: {dropped} value{'s' if dropped > 1 else ''} not positive on the "
|
|
392
|
+
f"log {role} axis, not drawn", key=("not positive", id(self), role))
|
|
393
|
+
extra = self.extra.get(role)
|
|
394
|
+
if extra is not None and len(extra):
|
|
395
|
+
n, a, b = kernels.extent(extra, scale.function)
|
|
396
|
+
if n:
|
|
397
|
+
count += n
|
|
398
|
+
lo, hi = min(lo, a), max(hi, b)
|
|
399
|
+
if not count:
|
|
400
|
+
return 0, math.nan, math.nan
|
|
401
|
+
return count, lo, hi
|
|
402
|
+
|
|
403
|
+
def values(self, role: str) -> Any:
|
|
404
|
+
"""Every value on axis ``role`` (for quantile limits)."""
|
|
405
|
+
out = array("d")
|
|
406
|
+
for layer in self.layers:
|
|
407
|
+
if not layer.options.get("bounds", True):
|
|
408
|
+
continue
|
|
409
|
+
v = layer.geom.values(layer, layer.frame_role(role))
|
|
410
|
+
if v is not None:
|
|
411
|
+
out.extend(v)
|
|
412
|
+
extra = self.extra.get(role)
|
|
413
|
+
if extra is not None:
|
|
414
|
+
out.extend(extra)
|
|
415
|
+
return out
|
|
416
|
+
|
|
417
|
+
def sticky(self, role: str) -> list[float]:
|
|
418
|
+
return self.sticky_values.get(role, [])
|
|
419
|
+
|
|
420
|
+
def zero_default(self, role: str) -> bool:
|
|
421
|
+
return role in self.zero
|
|
422
|
+
|
|
423
|
+
def zero_padding(self, role: str) -> bool:
|
|
424
|
+
return role in self.exact
|
|
425
|
+
|
|
426
|
+
def inverts(self, role: str) -> bool:
|
|
427
|
+
return role in self.invert
|
|
428
|
+
|
|
429
|
+
def whole_ticks(self, role: str) -> bool:
|
|
430
|
+
return role in self.whole
|
|
431
|
+
|
|
432
|
+
def time_role(self, role: str) -> Any:
|
|
433
|
+
return self.times.get(role)
|
|
434
|
+
|
|
435
|
+
def scale_of(self, role: str) -> str | None:
|
|
436
|
+
return self.scales.get(role)
|
|
437
|
+
|
|
438
|
+
def region_of(self, role: str) -> Any:
|
|
439
|
+
"""The genome region the call reads on axis ``role``."""
|
|
440
|
+
return self.regions.get(role, self.region)
|
|
441
|
+
|
|
442
|
+
def axis_title(self, role: str) -> Any:
|
|
443
|
+
return self.titles.get(role)
|
|
444
|
+
|
|
445
|
+
def label_room(self, fonts: Any, role: str) -> list[tuple[float, float, int]]:
|
|
446
|
+
return self.room(fonts, role)
|