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/canvas.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""The canvas plot_io draws on, and a canvas that records the drawing.
|
|
2
|
+
|
|
3
|
+
:class:`Canvas` is the protocol of what plot_io calls: drawing state attributes, the
|
|
4
|
+
transform, clips, groups and vec_io's drawing calls, typed as ``vec_io.Page``'s; a vec_io
|
|
5
|
+
page is one. ``document`` gives the font database text is shaped with and the points per
|
|
6
|
+
unit (:class:`CanvasDocument`).
|
|
7
|
+
|
|
8
|
+
:class:`Recording` is a canvas that draws nothing: it records each call and each state
|
|
9
|
+
assignment, in order, as plain data (``items``, ``dump()``, ``digest()``), so a figure's
|
|
10
|
+
drawing can be compared under any vec_io version. Text is still measured with vec_io's
|
|
11
|
+
fonts (the recording's ``font_db``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
from collections.abc import Generator, Sequence
|
|
19
|
+
from contextlib import AbstractContextManager
|
|
20
|
+
from typing import Any, Protocol, Unpack, overload
|
|
21
|
+
|
|
22
|
+
import vec_io
|
|
23
|
+
from vec_io.types import ArrayLike, Dashes, ItemStyle, LineStyle, PaintLike, SegmentStyle
|
|
24
|
+
|
|
25
|
+
__all__ = ["Canvas", "CanvasDocument", "Recording"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CanvasDocument(Protocol):
|
|
29
|
+
"""What plot_io reads of a canvas' document: the font database text is shaped with and
|
|
30
|
+
the points per canvas unit (``0.75`` for px)."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def font_db(self) -> Any: ...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def points_per_unit(self) -> float: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Canvas(Protocol):
|
|
40
|
+
"""The drawing surface plot_io draws on (a ``vec_io.Page`` is one): drawing state
|
|
41
|
+
attributes, the transform and clip, groups and vec_io's drawing calls, with
|
|
42
|
+
``vec_io.Page``'s signatures."""
|
|
43
|
+
|
|
44
|
+
fill: vec_io.Paint
|
|
45
|
+
stroke: vec_io.Paint
|
|
46
|
+
stroke_width: float
|
|
47
|
+
stroke_linecap: str
|
|
48
|
+
stroke_linejoin: str
|
|
49
|
+
stroke_dasharray: Any
|
|
50
|
+
opacity: float
|
|
51
|
+
text_anchor: str
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def document(self) -> CanvasDocument: ...
|
|
55
|
+
|
|
56
|
+
def state(self, **style: Unpack[vec_io.Style]) -> AbstractContextManager[Any]: ...
|
|
57
|
+
|
|
58
|
+
def translate(self, tx: float, ty: float) -> None: ...
|
|
59
|
+
|
|
60
|
+
def scale(self, sx: float, sy: float | None = None) -> None: ...
|
|
61
|
+
|
|
62
|
+
def rotate(self, degrees: float, cx: float = 0.0, cy: float = 0.0) -> None: ...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def transform(self, a: vec_io.Matrix) -> None: ...
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def transform(self, a: float, b: float, c: float, d: float, e: float, f: float) -> None: ...
|
|
69
|
+
|
|
70
|
+
def clip(self, path: vec_io.Path | str, *, clip_rule: str | None = None) -> None: ...
|
|
71
|
+
|
|
72
|
+
def group(self, *, opacity: float = 1.0, rasterize: bool = False,
|
|
73
|
+
dpi: float | None = None) -> AbstractContextManager[Any]: ...
|
|
74
|
+
|
|
75
|
+
def draw_rect(self, x: float, y: float, width: float, height: float, *, rx: float = 0.0,
|
|
76
|
+
ry: float | None = None, **style: Unpack[vec_io.Style]) -> None: ...
|
|
77
|
+
|
|
78
|
+
def draw_path(self, path: vec_io.Path | str, **style: Unpack[vec_io.Style]) -> None: ...
|
|
79
|
+
|
|
80
|
+
def draw_text(self, text: "str | vec_io.Content | vec_io.GlyphRun", x: float, y: float,
|
|
81
|
+
**style: Unpack[vec_io.Style]) -> None: ...
|
|
82
|
+
|
|
83
|
+
def draw_markers(self, x: ArrayLike, y: ArrayLike, *,
|
|
84
|
+
marker: "vec_io.Path | str | vec_io.Marker | Sequence[vec_io.Path | str]"
|
|
85
|
+
" | None" = None,
|
|
86
|
+
size: float | ArrayLike = 6.0, fill: PaintLike = ...,
|
|
87
|
+
stroke: PaintLike = ..., stroke_width: float | ArrayLike | None = None,
|
|
88
|
+
opacity: float | ArrayLike = 1.0,
|
|
89
|
+
simplify: bool | float | str | None = None,
|
|
90
|
+
**style: Unpack[ItemStyle]) -> None: ...
|
|
91
|
+
|
|
92
|
+
def draw_lines(self, x: ArrayLike | Sequence[ArrayLike],
|
|
93
|
+
y: ArrayLike | Sequence[ArrayLike], *, stroke: PaintLike = ...,
|
|
94
|
+
stroke_width: float | ArrayLike | None = None,
|
|
95
|
+
stroke_dasharray: Dashes | Sequence[Dashes] = None,
|
|
96
|
+
opacity: float | ArrayLike = 1.0,
|
|
97
|
+
stroke_linecap: str | None = None, stroke_linejoin: str | None = None,
|
|
98
|
+
simplify: bool | float | str | None = None,
|
|
99
|
+
interpolation: str | Sequence[str] = "linear", missing: str = "break",
|
|
100
|
+
**style: Unpack[LineStyle]) -> None: ...
|
|
101
|
+
|
|
102
|
+
def draw_segments(self, x0: float | ArrayLike, y0: float | ArrayLike,
|
|
103
|
+
x1: float | ArrayLike, y1: float | ArrayLike, *,
|
|
104
|
+
stroke: PaintLike = ..., stroke_width: float | ArrayLike | None = None,
|
|
105
|
+
opacity: float | ArrayLike = 1.0, stroke_linecap: str | None = None,
|
|
106
|
+
simplify: bool | float | str | None = None,
|
|
107
|
+
**style: Unpack[SegmentStyle]) -> None: ...
|
|
108
|
+
|
|
109
|
+
def draw_rects(self, x: float | ArrayLike, y: float | ArrayLike,
|
|
110
|
+
width: float | ArrayLike, height: float | ArrayLike, *,
|
|
111
|
+
fill: PaintLike = ..., stroke: PaintLike = ...,
|
|
112
|
+
stroke_width: float | ArrayLike | None = None,
|
|
113
|
+
opacity: float | ArrayLike = 1.0,
|
|
114
|
+
simplify: bool | float | str | None = None,
|
|
115
|
+
**style: Unpack[ItemStyle]) -> None: ...
|
|
116
|
+
|
|
117
|
+
def draw_polygons(self, paths: "Sequence[vec_io.Path | str] | tuple[ArrayLike, ...]", *,
|
|
118
|
+
fill: PaintLike = ..., stroke: PaintLike = None,
|
|
119
|
+
stroke_width: float | ArrayLike | None = None,
|
|
120
|
+
opacity: float | ArrayLike = 1.0,
|
|
121
|
+
simplify: bool | float | str | None = None,
|
|
122
|
+
**style: Unpack[ItemStyle]) -> None: ...
|
|
123
|
+
|
|
124
|
+
def draw_area(self, x: ArrayLike, y1: ArrayLike, y2: float | ArrayLike = 0.0, *,
|
|
125
|
+
fill: vec_io.Paint = ..., stroke: vec_io.Paint = None,
|
|
126
|
+
stroke_width: float | None = None, opacity: float = 1.0,
|
|
127
|
+
simplify: bool | float | str | None = None,
|
|
128
|
+
interpolation: str = "linear", missing: str = "break",
|
|
129
|
+
**style: Unpack[ItemStyle]) -> None: ...
|
|
130
|
+
|
|
131
|
+
def draw_cells(self, x_edges: ArrayLike, y_edges: ArrayLike, fill: Any, *,
|
|
132
|
+
palette: Sequence[vec_io.ColorLike] | None = None,
|
|
133
|
+
bounds: ArrayLike | None = None, bad: vec_io.ColorLike | None = None,
|
|
134
|
+
mode: str = "image", opacity: float = 1.0) -> None: ...
|
|
135
|
+
|
|
136
|
+
def draw_paragraphs(self, paragraphs: "vec_io.text.Paragraphs", x: float, y: float,
|
|
137
|
+
**style: Unpack[vec_io.Style]) -> None: ...
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
#: The drawing state attributes of :class:`Canvas` and their values on a new canvas.
|
|
141
|
+
_STATE = {"fill": "black", "stroke": None, "stroke_width": 1.0, "stroke_linecap": "butt",
|
|
142
|
+
"stroke_linejoin": "miter", "stroke_dasharray": None, "opacity": 1.0,
|
|
143
|
+
"text_anchor": "start"}
|
|
144
|
+
|
|
145
|
+
#: The names of the positional arguments of each call (to read each value in its unit).
|
|
146
|
+
_POSITIONAL = {
|
|
147
|
+
"state": (), "group": (), "translate": ("tx", "ty"), "scale": ("sx", "sy"),
|
|
148
|
+
"rotate": ("degrees", "cx", "cy"), "transform": ("a", "b", "c", "d", "e", "f"),
|
|
149
|
+
"clip": ("path",), "draw_rect": ("x", "y", "width", "height"), "draw_path": ("path",),
|
|
150
|
+
"draw_text": ("text", "x", "y"), "draw_markers": ("x", "y"), "draw_lines": ("x", "y"),
|
|
151
|
+
"draw_segments": ("x0", "y0", "x1", "y1"), "draw_rects": ("x", "y", "width", "height"),
|
|
152
|
+
"draw_polygons": ("paths",), "draw_area": ("x", "y1", "y2"),
|
|
153
|
+
"draw_cells": ("x_edges", "y_edges", "fill"),
|
|
154
|
+
"draw_paragraphs": ("paragraphs", "x", "y"),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#: Arguments in px, rounded to 1/64 px (:data:`plot_io.geometry.GRID`); colours are kept
|
|
158
|
+
#: as given; other numbers keep 12 significant digits.
|
|
159
|
+
_PX = frozenset({"tx", "ty", "cx", "cy", "e", "f", "path", "paths", "x", "y", "x0", "y0",
|
|
160
|
+
"x1", "y1", "y2", "width", "height", "rx", "ry", "size", "stroke_width",
|
|
161
|
+
"stroke_dasharray", "x_edges", "y_edges", "text", "paragraphs"})
|
|
162
|
+
_COLORS = frozenset({"fill", "stroke", "palette", "bad"})
|
|
163
|
+
_GRID = 64
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _number(v: float, px: bool) -> float:
|
|
167
|
+
if not math.isfinite(v):
|
|
168
|
+
return v
|
|
169
|
+
r = round(v * _GRID) / _GRID if px else float(f"{v:.12g}")
|
|
170
|
+
return 0.0 if r == 0 else r
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _run(run: Any) -> dict[str, Any]:
|
|
174
|
+
"""A glyph run as its text and its segments (see :func:`_glyphs`)."""
|
|
175
|
+
return {"text": run.text, "segments": _glyphs(run.glyphs)}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _glyphs(glyphs: Any) -> list[list[Any]]:
|
|
179
|
+
"""Glyphs as segments ``[text, font, size, advance, colour, synthetic]``: consecutive
|
|
180
|
+
glyphs of one face, size, colour and synthetic bold and italic."""
|
|
181
|
+
out: list[list[Any]] = []
|
|
182
|
+
key = None
|
|
183
|
+
for g in glyphs:
|
|
184
|
+
k = (g.face.postscript_name, g.style.size, g.style.color, list(g.synth))
|
|
185
|
+
if k != key:
|
|
186
|
+
out.append(["", k[0], k[1], 0.0, k[2], k[3]])
|
|
187
|
+
key = k
|
|
188
|
+
out[-1][0] += g.text
|
|
189
|
+
out[-1][3] += g.advance + g.adjust
|
|
190
|
+
for seg in out:
|
|
191
|
+
seg[2:5] = _number(seg[2], False), _number(seg[3], True), _plain(seg[4], "color")
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _lines(paragraphs: Any) -> list[Any]:
|
|
196
|
+
"""Placed paragraph lines: x offset, baseline and segments."""
|
|
197
|
+
return [[_number(ln.offset, True), _number(ln.baseline, True), _glyphs(ln.glyphs)]
|
|
198
|
+
for ln in paragraphs.line_cells() if ln.glyphs]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _plain(value: Any, kind: str = "px") -> Any:
|
|
202
|
+
"""``value`` as JSON data: numbers rounded by ``kind`` (``"px"``: to 1/64 px;
|
|
203
|
+
``"color"``: as given; else 12 significant digits), paths as their segments, glyph
|
|
204
|
+
runs as their text and segments, sequences and buffers as lists, objects as their
|
|
205
|
+
class name and fields."""
|
|
206
|
+
if value is None or isinstance(value, (bool, str)):
|
|
207
|
+
return value
|
|
208
|
+
if isinstance(value, int):
|
|
209
|
+
return value
|
|
210
|
+
if isinstance(value, float):
|
|
211
|
+
return value if kind == "color" else _number(value, kind == "px")
|
|
212
|
+
if isinstance(value, vec_io.Path):
|
|
213
|
+
return [[s[0], *(_plain(v, kind) for v in s[1:])] for s in value]
|
|
214
|
+
if isinstance(value, vec_io.GlyphRun):
|
|
215
|
+
return _run(value)
|
|
216
|
+
if isinstance(value, vec_io.Matrix):
|
|
217
|
+
return ["Matrix", *(_plain(float(v), "") for v in (value.a, value.b, value.c, value.d)),
|
|
218
|
+
_plain(float(value.e), kind), _plain(float(value.f), kind)]
|
|
219
|
+
if hasattr(value, "line_cells"):
|
|
220
|
+
return _lines(value)
|
|
221
|
+
if hasattr(value, "tolist") and not isinstance(value, type):
|
|
222
|
+
return _plain(value.tolist(), kind)
|
|
223
|
+
if isinstance(value, dict):
|
|
224
|
+
return {str(k): _plain(v, kind) for k, v in value.items()}
|
|
225
|
+
if isinstance(value, (list, tuple)):
|
|
226
|
+
return [_plain(v, kind) for v in value]
|
|
227
|
+
if isinstance(value, (bytes, bytearray)):
|
|
228
|
+
return value.hex()
|
|
229
|
+
fields = getattr(type(value), "__slots__", None) or getattr(value, "__dict__", None)
|
|
230
|
+
if fields:
|
|
231
|
+
names = [fields] if isinstance(fields, str) else list(fields)
|
|
232
|
+
return {"type": type(value).__name__,
|
|
233
|
+
**{n: _plain(getattr(value, n), kind) for n in names
|
|
234
|
+
if not n.startswith("_") and hasattr(value, n)}}
|
|
235
|
+
text = repr(value)
|
|
236
|
+
if " at 0x" in text:
|
|
237
|
+
raise TypeError(f"a recording cannot record {type(value).__name__} values")
|
|
238
|
+
return text
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _kind(name: str) -> str:
|
|
242
|
+
return "px" if name in _PX else "color" if name in _COLORS else ""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class _Document:
|
|
246
|
+
"""The document of a :class:`Recording`: px (``points_per_unit`` 0.75) and its font
|
|
247
|
+
database."""
|
|
248
|
+
|
|
249
|
+
points_per_unit = 0.75
|
|
250
|
+
|
|
251
|
+
def __init__(self, font_db: Any) -> None:
|
|
252
|
+
self.font_db = font_db
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Recording:
|
|
256
|
+
"""A :class:`Canvas` ``width`` × ``height`` px that draws nothing and records what is
|
|
257
|
+
drawn on it: each call and each state assignment is one item ``(member, args,
|
|
258
|
+
keywords)``, in order, as plain data (numbers in px rounded to 1/64 px, paths as their
|
|
259
|
+
segments, glyph runs as their text, font, size and advance, colours as given). A
|
|
260
|
+
``with`` block of ``state()`` or ``group()`` ends with the item ``("end",
|
|
261
|
+
(member,), {})``. Text is shaped with ``font_db`` (default: the font database plot_io
|
|
262
|
+
writes with).
|
|
263
|
+
|
|
264
|
+
``items``: the items; ``dump()``: one JSON line per item; ``digest()``: the SHA-256 of
|
|
265
|
+
the dump. :meth:`add` records an item: a subclass extends it to see the values as
|
|
266
|
+
given."""
|
|
267
|
+
|
|
268
|
+
fill: vec_io.Paint
|
|
269
|
+
stroke: vec_io.Paint
|
|
270
|
+
stroke_width: float
|
|
271
|
+
stroke_linecap: str
|
|
272
|
+
stroke_linejoin: str
|
|
273
|
+
stroke_dasharray: Any
|
|
274
|
+
opacity: float
|
|
275
|
+
text_anchor: str
|
|
276
|
+
width: float
|
|
277
|
+
height: float
|
|
278
|
+
items: list[tuple[str, tuple[Any, ...], dict[str, Any]]]
|
|
279
|
+
_document: _Document
|
|
280
|
+
_saved: list[dict[str, Any]]
|
|
281
|
+
|
|
282
|
+
def __init__(self, width: float, height: float, font_db: Any = None) -> None:
|
|
283
|
+
if font_db is None:
|
|
284
|
+
from .render import font_db_of, fonts_of
|
|
285
|
+
font_db = font_db_of(fonts_of(None, {}))
|
|
286
|
+
object.__setattr__(self, "width", float(width))
|
|
287
|
+
object.__setattr__(self, "height", float(height))
|
|
288
|
+
object.__setattr__(self, "items", [])
|
|
289
|
+
object.__setattr__(self, "_document", _Document(font_db))
|
|
290
|
+
object.__setattr__(self, "_saved", [])
|
|
291
|
+
for name, value in _STATE.items():
|
|
292
|
+
object.__setattr__(self, name, value)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def document(self) -> CanvasDocument:
|
|
296
|
+
return self._document
|
|
297
|
+
|
|
298
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
299
|
+
if name in _STATE:
|
|
300
|
+
self.add(name, (value,), {})
|
|
301
|
+
object.__setattr__(self, name, value)
|
|
302
|
+
|
|
303
|
+
def add(self, member: str, args: tuple[Any, ...], keywords: dict[str, Any]) -> None:
|
|
304
|
+
"""Record call ``member(*args, **keywords)`` (or, for a state attribute, its
|
|
305
|
+
assignment: ``args`` is the value)."""
|
|
306
|
+
names = (member,) if member in _STATE else _POSITIONAL.get(member, ())
|
|
307
|
+
plain_args = tuple(_plain(v, _kind(names[k]) if k < len(names) else "")
|
|
308
|
+
for k, v in enumerate(args))
|
|
309
|
+
plain_kw = {k: _plain(v, _kind(k)) for k, v in keywords.items()}
|
|
310
|
+
self.items.append((member, plain_args, plain_kw))
|
|
311
|
+
|
|
312
|
+
def dump(self) -> str:
|
|
313
|
+
"""The items, one JSON line each (``[member, args, keywords]``)."""
|
|
314
|
+
return "".join(json.dumps([m, list(a), k], sort_keys=True, separators=(",", ":"))
|
|
315
|
+
+ "\n" for m, a, k in self.items)
|
|
316
|
+
|
|
317
|
+
def digest(self) -> str:
|
|
318
|
+
"""The SHA-256 (hex) of :meth:`dump`."""
|
|
319
|
+
return hashlib.sha256(self.dump().encode()).hexdigest()
|
|
320
|
+
|
|
321
|
+
@contextlib.contextmanager
|
|
322
|
+
def _scope(self, member: str, keywords: dict[str, Any]) -> Generator["Recording"]:
|
|
323
|
+
self.add(member, (), keywords)
|
|
324
|
+
self._saved.append({k: getattr(self, k) for k in _STATE})
|
|
325
|
+
for k, v in keywords.items():
|
|
326
|
+
if k in _STATE:
|
|
327
|
+
object.__setattr__(self, k, v)
|
|
328
|
+
try:
|
|
329
|
+
yield self
|
|
330
|
+
finally:
|
|
331
|
+
for k, v in self._saved.pop().items():
|
|
332
|
+
object.__setattr__(self, k, v)
|
|
333
|
+
self.add("end", (member,), {})
|
|
334
|
+
|
|
335
|
+
def state(self, **style: Any) -> AbstractContextManager["Recording"]:
|
|
336
|
+
return self._scope("state", style)
|
|
337
|
+
|
|
338
|
+
def group(self, **options: Any) -> AbstractContextManager["Recording"]:
|
|
339
|
+
return self._scope("group", options)
|
|
340
|
+
|
|
341
|
+
def translate(self, *args: Any, **kw: Any) -> None:
|
|
342
|
+
self.add("translate", args, kw)
|
|
343
|
+
|
|
344
|
+
def scale(self, *args: Any, **kw: Any) -> None:
|
|
345
|
+
self.add("scale", args, kw)
|
|
346
|
+
|
|
347
|
+
def rotate(self, *args: Any, **kw: Any) -> None:
|
|
348
|
+
self.add("rotate", args, kw)
|
|
349
|
+
|
|
350
|
+
def transform(self, *args: Any, **kw: Any) -> None:
|
|
351
|
+
self.add("transform", args, kw)
|
|
352
|
+
|
|
353
|
+
def clip(self, *args: Any, **kw: Any) -> None:
|
|
354
|
+
self.add("clip", args, kw)
|
|
355
|
+
|
|
356
|
+
def draw_rect(self, *args: Any, **kw: Any) -> None:
|
|
357
|
+
self.add("draw_rect", args, kw)
|
|
358
|
+
|
|
359
|
+
def draw_path(self, *args: Any, **kw: Any) -> None:
|
|
360
|
+
self.add("draw_path", args, kw)
|
|
361
|
+
|
|
362
|
+
def draw_text(self, *args: Any, **kw: Any) -> None:
|
|
363
|
+
self.add("draw_text", args, kw)
|
|
364
|
+
|
|
365
|
+
def draw_markers(self, *args: Any, **kw: Any) -> None:
|
|
366
|
+
self.add("draw_markers", args, kw)
|
|
367
|
+
|
|
368
|
+
def draw_lines(self, *args: Any, **kw: Any) -> None:
|
|
369
|
+
self.add("draw_lines", args, kw)
|
|
370
|
+
|
|
371
|
+
def draw_segments(self, *args: Any, **kw: Any) -> None:
|
|
372
|
+
self.add("draw_segments", args, kw)
|
|
373
|
+
|
|
374
|
+
def draw_rects(self, *args: Any, **kw: Any) -> None:
|
|
375
|
+
self.add("draw_rects", args, kw)
|
|
376
|
+
|
|
377
|
+
def draw_polygons(self, *args: Any, **kw: Any) -> None:
|
|
378
|
+
self.add("draw_polygons", args, kw)
|
|
379
|
+
|
|
380
|
+
def draw_area(self, *args: Any, **kw: Any) -> None:
|
|
381
|
+
self.add("draw_area", args, kw)
|
|
382
|
+
|
|
383
|
+
def draw_cells(self, *args: Any, **kw: Any) -> None:
|
|
384
|
+
self.add("draw_cells", args, kw)
|
|
385
|
+
|
|
386
|
+
def draw_paragraphs(self, *args: Any, **kw: Any) -> None:
|
|
387
|
+
"""Paragraphs (``vec_io.text`` ``Paragraphs``) with the top-left corner of their box
|
|
388
|
+
at ``(x, y)``: recorded as their placed lines (x offset, baseline, segments)."""
|
|
389
|
+
self.add("draw_paragraphs", args, kw)
|