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
plot_io/registry.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The registries of named things: ``color_maps``, ``markers``, ``styles`` and
|
|
2
|
+
``layers`` (plot types), each with ``add(name, value)``, ``names()`` and ``get(name)``."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__all__ = ["Registry", "color_maps", "markers", "styles", "layers"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Registry:
|
|
11
|
+
"""Named values of one kind: ``add`` registers one (a name taken is replaced),
|
|
12
|
+
``names`` lists them, ``get`` returns one (``ValueError`` for an unknown name)."""
|
|
13
|
+
|
|
14
|
+
__slots__ = ("kind", "_add", "_names", "_get")
|
|
15
|
+
|
|
16
|
+
def __init__(self, kind: str, add: Callable[..., None],
|
|
17
|
+
names: Callable[..., list[str]], get: Callable[[str], Any]) -> None:
|
|
18
|
+
self.kind = kind
|
|
19
|
+
self._add, self._names, self._get = add, names, get
|
|
20
|
+
|
|
21
|
+
def add(self, name: str, value: Any, **options: Any) -> None:
|
|
22
|
+
"""Register ``value`` as ``name`` (``options``: those of the kind's ``add``, such as
|
|
23
|
+
a color map's ``replace``)."""
|
|
24
|
+
self._add(name, value, **options)
|
|
25
|
+
|
|
26
|
+
def names(self, **options: Any) -> list[str]:
|
|
27
|
+
"""The registered names, sorted (``options``: those of the kind's, such as a color
|
|
28
|
+
map's ``kind``)."""
|
|
29
|
+
return self._names(**options)
|
|
30
|
+
|
|
31
|
+
def get(self, name: str) -> Any:
|
|
32
|
+
"""The value registered as ``name``."""
|
|
33
|
+
return self._get(name)
|
|
34
|
+
|
|
35
|
+
def __contains__(self, name: object) -> bool:
|
|
36
|
+
"""Whether ``get(name)`` finds a value (aliases such as ``"o"`` included)."""
|
|
37
|
+
if not isinstance(name, str):
|
|
38
|
+
return False
|
|
39
|
+
try:
|
|
40
|
+
self._get(name)
|
|
41
|
+
except (KeyError, ValueError):
|
|
42
|
+
return False
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return f"<registry of {self.kind}s: {len(self.names())}>"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _color_maps() -> Registry:
|
|
50
|
+
from .color import maps
|
|
51
|
+
return Registry("color map", maps.add_color_map, maps.color_map_names, maps.ColorMap)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _markers() -> Registry:
|
|
55
|
+
from . import markers as m
|
|
56
|
+
return Registry("marker", m.add_marker, lambda: sorted(m.MARKERS),
|
|
57
|
+
lambda name: m.resolve(name)[0])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _styles() -> Registry:
|
|
61
|
+
from . import styles as s
|
|
62
|
+
return Registry("style", s.add_style, s.names, s.settings)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _layers() -> Registry:
|
|
66
|
+
from . import layers as la
|
|
67
|
+
return Registry("layer", la.add_layer, lambda: sorted(la.layer_types()), la.layer_type)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#: Color maps of every kind: ``add(name, ColorMap or colors, replace=False, kind=None)`` (a
|
|
71
|
+
#: bundled name only with ``replace=True``; not a ``"_r"`` name); ``get`` gives a
|
|
72
|
+
#: ``ColorMap`` (``"_r"``: reversed; a variant word first: in that variant).
|
|
73
|
+
color_maps = _color_maps()
|
|
74
|
+
#: Marker shapes: ``add(name, vec_io Path or path data in a unit box)``; ``get`` gives the
|
|
75
|
+
#: ``Path``.
|
|
76
|
+
markers = _markers()
|
|
77
|
+
#: Styles: ``add(name, dict of settings with an optional "base")``; ``get`` gives every key
|
|
78
|
+
#: of the style's chain, ``DEFAULTS`` included (values as given).
|
|
79
|
+
styles = _styles()
|
|
80
|
+
#: Plot types: ``add(name, factory)`` adds ``plot.<name>(…)``, ``figure.<name>(…)`` and
|
|
81
|
+
#: ``facets.<name>(…)``; ``get`` gives the factory.
|
|
82
|
+
layers = _layers()
|
plot_io/render.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Drawing and output of figures.
|
|
2
|
+
|
|
3
|
+
A figure is laid out first (:func:`plot_io.layout.layout`: every area measured into bands,
|
|
4
|
+
the grid solved, each area placed into a frozen geometry); drawing reads that geometry and
|
|
5
|
+
the settings' colors and widths, and draws the layers of each plot in its axes. All
|
|
6
|
+
lengths here are px. Output is vec_io's (:meth:`vec_io.Document.write`): plot_io maps its
|
|
7
|
+
own options (``quality``, resolutions, the background, ``simplify``, ``fonts``) and passes
|
|
8
|
+
the rest.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import os
|
|
13
|
+
from collections.abc import Generator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import vec_io
|
|
17
|
+
from vec_io.convert import FORMATS
|
|
18
|
+
|
|
19
|
+
from . import text as _text
|
|
20
|
+
from .canvas import Canvas
|
|
21
|
+
from .color.values import resolve_color
|
|
22
|
+
from .figure import output_quality
|
|
23
|
+
from .layers.context import DrawContext
|
|
24
|
+
from .layout import Geometry, layout
|
|
25
|
+
from .settings import dpi, overriding, simplify_value
|
|
26
|
+
from .spec import rendering, set_boxes
|
|
27
|
+
from .states import z_key
|
|
28
|
+
from .text import draw_paragraphs, draw_run
|
|
29
|
+
from .titles import document_title
|
|
30
|
+
|
|
31
|
+
__all__ = ["draw", "save", "write", "size", "Geometry", "layout", "draw_geometry"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def draw_layers(call: Any, ctx: Any, overlay: bool = False) -> None:
|
|
35
|
+
"""Draw a call's layers (``overlay``: its value labels, drawn after every layer of the
|
|
36
|
+
plot)."""
|
|
37
|
+
c = ctx.for_call(call)
|
|
38
|
+
for layer in call.layers:
|
|
39
|
+
if layer.overlay == overlay:
|
|
40
|
+
seg = layer.options.get("segment")
|
|
41
|
+
if seg is None:
|
|
42
|
+
lay = layer.columns(c)
|
|
43
|
+
lay.geom.draw(c, lay)
|
|
44
|
+
continue
|
|
45
|
+
with seg.scope(c, clip=True) as cs: # a region of several, side by side
|
|
46
|
+
lay = layer.columns(cs)
|
|
47
|
+
lay.geom.draw(cs, lay)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _draw_item(item: Any, ctx: Any) -> None:
|
|
51
|
+
if isinstance(item, list): # members of one call, in data order
|
|
52
|
+
from .layers.geoms import draw_in_order
|
|
53
|
+
if len(item) == 1 or not draw_in_order(ctx, item):
|
|
54
|
+
for c in item:
|
|
55
|
+
draw_layers(c, ctx)
|
|
56
|
+
elif hasattr(item, "layers") and hasattr(item, "series"):
|
|
57
|
+
draw_layers(item, ctx)
|
|
58
|
+
else:
|
|
59
|
+
item.draw(ctx)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _in_order(items: list[Any]) -> list[Any]:
|
|
63
|
+
"""``items`` with the consecutive members of one call (``series._order_rows``: ``group=``
|
|
64
|
+
and level members) drawn alike (``z``, ``clip``, ``rasterize``) gathered in lists."""
|
|
65
|
+
out: list[Any] = []
|
|
66
|
+
last = None
|
|
67
|
+
for item in items:
|
|
68
|
+
s: Any = getattr(item, "series", None)
|
|
69
|
+
rows = None if s is None or not hasattr(item, "layers") else s.__dict__.get("_order_rows")
|
|
70
|
+
if s is None or rows is None:
|
|
71
|
+
out.append(item)
|
|
72
|
+
last = None
|
|
73
|
+
continue
|
|
74
|
+
key = (id(rows[0]), z_key(item),
|
|
75
|
+
*(s.resolve(k) if k in s.SETTINGS else None for k in ("clip", "rasterize")))
|
|
76
|
+
if key == last:
|
|
77
|
+
out[-1].append(item)
|
|
78
|
+
else:
|
|
79
|
+
out.append([item])
|
|
80
|
+
last = key
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _grid(canvas: Canvas, ctx: DrawContext, lines: Any) -> None:
|
|
85
|
+
from .lines import dash_of
|
|
86
|
+
x0, y0, x1, y1 = ctx.rect
|
|
87
|
+
for axis, prefix, horizontal, positions in lines:
|
|
88
|
+
width = axis.resolve(f"{prefix}_width")
|
|
89
|
+
with canvas.state():
|
|
90
|
+
canvas.stroke = resolve_color(axis.resolve(f"{prefix}_color"), ctx.cycle)
|
|
91
|
+
canvas.stroke_width = width
|
|
92
|
+
style = "solid" if prefix == "zero_line" else axis.resolve(f"{prefix}_style")
|
|
93
|
+
canvas.stroke_dasharray = dash_of(style, width)
|
|
94
|
+
path = vec_io.Path()
|
|
95
|
+
for p in positions:
|
|
96
|
+
if horizontal:
|
|
97
|
+
path.move_to(x0, p).line_to(x1, p)
|
|
98
|
+
else:
|
|
99
|
+
path.move_to(p, y0).line_to(p, y1)
|
|
100
|
+
canvas.draw_path(path, fill=None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _spines(canvas: Canvas, spines: Any, cycle: Any) -> None:
|
|
104
|
+
with canvas.state():
|
|
105
|
+
canvas.stroke_linecap = "square"
|
|
106
|
+
for axis, (a, b, c, d) in spines:
|
|
107
|
+
lw = axis.resolve("line_width")
|
|
108
|
+
if not lw:
|
|
109
|
+
continue
|
|
110
|
+
canvas.stroke = resolve_color(axis.resolve("line_color"), cycle)
|
|
111
|
+
canvas.stroke_width = lw
|
|
112
|
+
canvas.draw_path(vec_io.Path().move_to(a, b).line_to(c, d), fill=None)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _axis(canvas: Canvas, state: Any, g: Any, cycle: Any) -> None:
|
|
116
|
+
if not g.shown:
|
|
117
|
+
return
|
|
118
|
+
axis = state.axis
|
|
119
|
+
for marks in g.marks:
|
|
120
|
+
with canvas.state():
|
|
121
|
+
canvas.stroke = resolve_color(axis.resolve("tick_color"), cycle)
|
|
122
|
+
canvas.stroke_width = axis.resolve("tick_width")
|
|
123
|
+
path = vec_io.Path()
|
|
124
|
+
for a, b, c, d in marks:
|
|
125
|
+
path.move_to(a, b).line_to(c, d)
|
|
126
|
+
canvas.draw_path(path, fill=None)
|
|
127
|
+
for r in g.labels:
|
|
128
|
+
draw_run(canvas, r)
|
|
129
|
+
draw_run(canvas, g.title)
|
|
130
|
+
draw_run(canvas, g.offset)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def draw_plot(canvas: Canvas, g: Any) -> None:
|
|
134
|
+
"""Draw a plot placed as ``g`` (:class:`~plot_io.geometry.PlotGeometry`)."""
|
|
135
|
+
ps = g.state
|
|
136
|
+
plot = ps.plot
|
|
137
|
+
x0, y0, x1, y1 = rect = g.rect
|
|
138
|
+
w, h = x1 - x0, y1 - y0
|
|
139
|
+
set_boxes(plot, axes_width=w, axes_height=h)
|
|
140
|
+
cycle = ps.cycle
|
|
141
|
+
ctx = DrawContext(canvas, plot, rect, ps.x, ps.y, cycle)
|
|
142
|
+
ctx.figure_size = g.figure_size
|
|
143
|
+
if g.legend is not None: # point labels keep clear of the legend
|
|
144
|
+
lx, ly, lw, lh = g.legend.box
|
|
145
|
+
ctx.avoid = [(lx, ly, lx + lw, ly + lh)]
|
|
146
|
+
bg = plot.resolve("background")
|
|
147
|
+
if bg is not None:
|
|
148
|
+
with canvas.state():
|
|
149
|
+
canvas.fill = resolve_color(bg, cycle)
|
|
150
|
+
canvas.draw_rect(x0, y0, w, h, stroke=None)
|
|
151
|
+
if ps.axes_on:
|
|
152
|
+
_grid(canvas, ctx, g.grid)
|
|
153
|
+
items = _in_order(sorted(list(ps.calls) + ps.decorations, key=z_key))
|
|
154
|
+
clipped = None
|
|
155
|
+
scope = contextlib.ExitStack() # the clip of the calls drawn now
|
|
156
|
+
try:
|
|
157
|
+
for s in items:
|
|
158
|
+
obj = getattr(s[0] if isinstance(s, list) else s, "series", s)
|
|
159
|
+
own = obj.resolve("clip") if "clip" in obj.SETTINGS else True
|
|
160
|
+
want = bool(plot.resolve("clip")) and own is not False
|
|
161
|
+
if want != clipped:
|
|
162
|
+
scope.close()
|
|
163
|
+
scope.enter_context(canvas.state())
|
|
164
|
+
if want:
|
|
165
|
+
canvas.clip(vec_io.Path().rect(x0, y0, w, h))
|
|
166
|
+
clipped = want
|
|
167
|
+
r = obj.resolve("rasterize") if "rasterize" in obj.SETTINGS else None
|
|
168
|
+
if r:
|
|
169
|
+
res = None if r is True else float(r)
|
|
170
|
+
with canvas.group(rasterize=True, dpi=res):
|
|
171
|
+
_draw_item(s, ctx)
|
|
172
|
+
else:
|
|
173
|
+
_draw_item(s, ctx)
|
|
174
|
+
finally:
|
|
175
|
+
scope.close()
|
|
176
|
+
tctxs = []
|
|
177
|
+
for tg in g.twins:
|
|
178
|
+
t = tg.state
|
|
179
|
+
tctx = DrawContext(canvas, t.plot, rect, t.x, t.y, cycle)
|
|
180
|
+
tctx.figure_size = ctx.figure_size
|
|
181
|
+
tctxs.append((tg, tctx))
|
|
182
|
+
_grid(canvas, tctx, tg.grid)
|
|
183
|
+
with canvas.state():
|
|
184
|
+
if plot.resolve("clip"):
|
|
185
|
+
canvas.clip(vec_io.Path().rect(x0, y0, w, h))
|
|
186
|
+
for c in _in_order(list(t.calls)):
|
|
187
|
+
_draw_item(c, tctx)
|
|
188
|
+
for c in ps.calls:
|
|
189
|
+
draw_layers(c, ctx, overlay=True)
|
|
190
|
+
border = plot.resolve("border_color")
|
|
191
|
+
if border is not None and plot.resolve("border_width"):
|
|
192
|
+
with canvas.state():
|
|
193
|
+
canvas.stroke = resolve_color(border, cycle)
|
|
194
|
+
canvas.stroke_width = plot.resolve("border_width")
|
|
195
|
+
canvas.draw_path(vec_io.Path().rect(x0, y0, w, h), fill=None)
|
|
196
|
+
if ps.axes_on:
|
|
197
|
+
_spines(canvas, g.spines, cycle)
|
|
198
|
+
_axis(canvas, ps.x, g.x, cycle)
|
|
199
|
+
_axis(canvas, ps.y, g.y, cycle)
|
|
200
|
+
for tg, tctx in tctxs:
|
|
201
|
+
t = tg.state
|
|
202
|
+
_spines(canvas, tg.spines, t.cycle)
|
|
203
|
+
_axis(canvas, t.x, tg.x, cycle)
|
|
204
|
+
_axis(canvas, t.y, tg.y, cycle)
|
|
205
|
+
t.legend.draw(canvas, tctx, tg.legend)
|
|
206
|
+
for r in g.row_label:
|
|
207
|
+
draw_run(canvas, r)
|
|
208
|
+
for r in g.lane_names:
|
|
209
|
+
draw_run(canvas, r)
|
|
210
|
+
for r in (g.title if isinstance(g.title, tuple) else (g.title,)):
|
|
211
|
+
draw_run(canvas, r)
|
|
212
|
+
_rule(canvas, plot.title, g.title_rule)
|
|
213
|
+
for tg in g.titles:
|
|
214
|
+
_title(canvas, tg)
|
|
215
|
+
ps.legend.draw(canvas, ctx, g.legend)
|
|
216
|
+
for bg_ in g.bars:
|
|
217
|
+
bg_.state.draw(canvas, bg_)
|
|
218
|
+
for c in [ctx] + [t for _, t in tctxs]:
|
|
219
|
+
c.release()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _title(canvas: Canvas, tg: Any) -> None:
|
|
223
|
+
"""A title placed as ``tg`` (:class:`~plot_io.geometry.TitleGeometry`): its runs or its
|
|
224
|
+
paragraph, then its rule."""
|
|
225
|
+
for r in tg.runs:
|
|
226
|
+
draw_run(canvas, r)
|
|
227
|
+
if tg.paragraph is not None:
|
|
228
|
+
x, y = tg.at
|
|
229
|
+
draw_paragraphs(canvas, tg.paragraph, x, y, tg.obj.resolve("color"))
|
|
230
|
+
_rule(canvas, tg.obj, tg.rule)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _rule(canvas: Canvas, title: Any, rect: Any) -> None:
|
|
234
|
+
"""A title's rule (nothing for ``None``), in its ``rule_color``."""
|
|
235
|
+
if rect is None:
|
|
236
|
+
return
|
|
237
|
+
with canvas.state():
|
|
238
|
+
canvas.fill = resolve_color(title.resolve("rule_color"), ())
|
|
239
|
+
canvas.draw_rect(*rect, stroke=None)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def draw_geometry(fig: Any, geom: Geometry, canvas: Canvas, fill: bool = True) -> None:
|
|
243
|
+
"""Draw a laid-out figure (see :func:`plot_io.layout.layout`); ``fill``: its background
|
|
244
|
+
first."""
|
|
245
|
+
from .color.maps import cycle_of, map_of
|
|
246
|
+
from .lines import dash_of
|
|
247
|
+
bg = resolve_color(fig.resolve("background"), cycle_of(map_of(fig, "qualitative"))) \
|
|
248
|
+
if fill else None
|
|
249
|
+
if bg is not None:
|
|
250
|
+
with canvas.state():
|
|
251
|
+
canvas.fill = bg
|
|
252
|
+
canvas.draw_rect(0, 0, geom.width, geom.height, stroke=None)
|
|
253
|
+
f = geom.figure
|
|
254
|
+
for d, (x, y, w, h) in f.spans:
|
|
255
|
+
with canvas.state():
|
|
256
|
+
canvas.fill = resolve_color(d.resolve("color"), ())
|
|
257
|
+
canvas.opacity = d.resolve("opacity")
|
|
258
|
+
canvas.draw_rect(x, y, w, h, stroke=None)
|
|
259
|
+
for a in geom.areas:
|
|
260
|
+
if a.is_bar:
|
|
261
|
+
a.state.draw(canvas, a.geometry)
|
|
262
|
+
else:
|
|
263
|
+
draw_plot(canvas, a.geometry)
|
|
264
|
+
for d, (x0, y0, x1, y1) in f.lines:
|
|
265
|
+
w = d.resolve("line_width")
|
|
266
|
+
canvas.draw_lines([x0, x1], [y0, y1], stroke=resolve_color(d.resolve("color"), ()),
|
|
267
|
+
stroke_width=w, stroke_dasharray=dash_of(d.resolve("line_style"), w),
|
|
268
|
+
opacity=d.resolve("opacity"))
|
|
269
|
+
for r in f.groups:
|
|
270
|
+
draw_run(canvas, r)
|
|
271
|
+
for tg in f.titles:
|
|
272
|
+
_title(canvas, tg)
|
|
273
|
+
if f.legends:
|
|
274
|
+
contexts = {}
|
|
275
|
+
for a in geom.areas:
|
|
276
|
+
if not a.is_bar:
|
|
277
|
+
ps = a.state
|
|
278
|
+
c = DrawContext(canvas, ps.plot, a.axes, ps.x, ps.y, ps.cycle)
|
|
279
|
+
c.figure_size = (geom.width, geom.height)
|
|
280
|
+
contexts[id(ps.plot)] = c
|
|
281
|
+
for lg in f.legends:
|
|
282
|
+
lg.state.draw_figure(canvas, lg.legend, contexts)
|
|
283
|
+
for cg in f.colorbars:
|
|
284
|
+
cg.bar.state.draw(canvas, cg.bar)
|
|
285
|
+
_rule(canvas, fig.title, f.rule)
|
|
286
|
+
if f.title is not None:
|
|
287
|
+
draw_paragraphs(canvas, f.title, *f.title_at, fig.title.resolve("color"))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def draw(fig: Any, canvas: Canvas, x: float = 0.0, y: float = 0.0,
|
|
291
|
+
only: Any = None) -> tuple[float, float]:
|
|
292
|
+
"""Draw on a canvas (a vec_io page, a :class:`~plot_io.canvas.Recording`) at ``(x, y)``
|
|
293
|
+
(page units); returns the size in px. Text is shaped with the fonts of the canvas'
|
|
294
|
+
document."""
|
|
295
|
+
with canvas.state():
|
|
296
|
+
scale = 0.75 / canvas.document.points_per_unit # px → page units
|
|
297
|
+
canvas.translate(x, y)
|
|
298
|
+
if scale != 1.0:
|
|
299
|
+
canvas.scale(scale)
|
|
300
|
+
canvas.text_anchor = "start"
|
|
301
|
+
with rendering(fig):
|
|
302
|
+
geom = layout(fig, canvas.document.font_db, only)
|
|
303
|
+
draw_geometry(fig, geom, canvas)
|
|
304
|
+
return geom.width, geom.height
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
#: Options of ``Document``: document-level choices.
|
|
308
|
+
_DOC_KEYS = ("title", "author", "subject", "keywords", "creator", "base_dir", "creation_date")
|
|
309
|
+
|
|
310
|
+
_IMAGES = ("png", "jpeg", "jxl", "avif")
|
|
311
|
+
#: vec_io's view and page options, which plot_io does not take.
|
|
312
|
+
_SIZE_KEYS = ("view", "pages")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def output_kind(format: Any) -> str:
|
|
316
|
+
"""The vec_io output kind of a format name or extension (``"jpg"`` is ``"jpeg"``)."""
|
|
317
|
+
kind = FORMATS.get(str(format).lower().lstrip("."))
|
|
318
|
+
if kind is None:
|
|
319
|
+
raise ValueError(f"unknown format {format!r}: use pdf, eps, svg, png, jpeg, jxl or "
|
|
320
|
+
"avif")
|
|
321
|
+
return kind
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def fonts_of(fig: Any, value: Any = None) -> vec_io.Fonts:
|
|
325
|
+
"""The fonts of an output: ``value`` (a vec_io ``Fonts`` or a dict of its fields), else
|
|
326
|
+
the figure's ``fonts``; a dict without ``system`` takes
|
|
327
|
+
:data:`plot_io.text.SYSTEM_FONTS`."""
|
|
328
|
+
if value is None:
|
|
329
|
+
value = fig.resolve("fonts")
|
|
330
|
+
if value is None or isinstance(value, dict):
|
|
331
|
+
value = {"system": _text.SYSTEM_FONTS, **(value or {})}
|
|
332
|
+
return vec_io.Fonts.of(value, "fonts")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def font_db_of(fonts: vec_io.Fonts) -> Any:
|
|
336
|
+
"""The font database of ``fonts``: its ``db``, else one shared between renders with
|
|
337
|
+
the same sources."""
|
|
338
|
+
if fonts.db is not None:
|
|
339
|
+
return fonts.db
|
|
340
|
+
return _text.document_db(fonts.dirs, fonts.system, fonts.downloads)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _page(fig: Any, only: Any, doc_opts: dict[str, Any], fill: bool = True,
|
|
344
|
+
fonts: Any = None, output: tuple[str, dict[str, Any]] | None = None) -> Any:
|
|
345
|
+
"""A px document holding the figure on its one page (text shaped with ``fonts``, see
|
|
346
|
+
:func:`fonts_of`). ``fill``: the figure's background drawn. ``output``: ``(format,
|
|
347
|
+
write options)``, the output the page is recorded for (colours converted, text as
|
|
348
|
+
outlines with ``text="paths"``)."""
|
|
349
|
+
from vec_io.convert import recording_for
|
|
350
|
+
|
|
351
|
+
db = font_db_of(fonts_of(fig, fonts))
|
|
352
|
+
doc = vec_io.Document(unit="px", fonts=vec_io.Fonts(db=db), **doc_opts)
|
|
353
|
+
recording = contextlib.nullcontext() if output is None else recording_for(
|
|
354
|
+
doc, output[0], **output[1])
|
|
355
|
+
with rendering(fig), recording:
|
|
356
|
+
geom = layout(fig, doc.font_db, only)
|
|
357
|
+
page = doc.add_page(max(geom.width, 1.0), max(geom.height, 1.0))
|
|
358
|
+
draw_geometry(fig, geom, page, fill)
|
|
359
|
+
return page
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def size(fig: Any, only: Any = None) -> tuple[float, float]:
|
|
363
|
+
geom = layout(fig, font_db_of(fonts_of(fig)), only)
|
|
364
|
+
return geom.width, geom.height
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def quality_of(fig: Any, options: dict[str, Any]) -> Any:
|
|
368
|
+
"""The ``quality`` option, else the figure's (an option of ``None`` is unset)."""
|
|
369
|
+
v = options.get("quality")
|
|
370
|
+
return fig.resolve("quality") if v is None else v
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@contextlib.contextmanager
|
|
374
|
+
def _simplified(fig: Any, value: Any) -> Generator[None]:
|
|
375
|
+
"""The figure's ``simplify`` taken as ``value`` while the body runs, in this thread
|
|
376
|
+
only (``None``: as it is); series with their own keep it."""
|
|
377
|
+
with overriding(fig, simplify=value):
|
|
378
|
+
yield
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _with_quality(value: Any, cls: Any, what: str, quality: int | None) -> Any:
|
|
382
|
+
"""Option ``value`` (an object, a dict of its fields or ``None``) with plot_io's
|
|
383
|
+
``quality``: a dict or ``None`` takes it unless it sets its own; an object is used as
|
|
384
|
+
given."""
|
|
385
|
+
if value is not None and not isinstance(value, dict):
|
|
386
|
+
return cls.of(value, what)
|
|
387
|
+
fields = dict(value or {})
|
|
388
|
+
if quality is not None:
|
|
389
|
+
fields.setdefault("quality", quality)
|
|
390
|
+
return cls.of(fields, what) if fields else None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _flatten(value: Any) -> Any:
|
|
394
|
+
"""``flatten``: a vec_io ``Flatten``, or a dict of its fields (``dpi`` as plot_io's
|
|
395
|
+
resolutions: ``"2x"`` is 192)."""
|
|
396
|
+
if isinstance(value, dict) and "dpi" in value:
|
|
397
|
+
value = {**value, "dpi": dpi(value["dpi"])}
|
|
398
|
+
return vec_io.Flatten.of(value, "flatten")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def write(fig: Any, format: str = "pdf", only: Any = None, **options: Any) -> bytes:
|
|
402
|
+
"""The figure (or the plot ``only``) as ``format`` bytes; ``options``: plot_io's
|
|
403
|
+
(``quality``, ``dpi``, ``default_dpi``, ``background``, ``simplify``, ``fonts``), the
|
|
404
|
+
``Document``'s, and the rest :meth:`vec_io.Document.write`'s."""
|
|
405
|
+
kind = output_kind(format)
|
|
406
|
+
out = dict(options)
|
|
407
|
+
doc_opts = {k: out.pop(k) for k in _DOC_KEYS if k in out}
|
|
408
|
+
if "title" not in doc_opts:
|
|
409
|
+
doc_opts["title"] = document_title(fig, only)
|
|
410
|
+
elif not doc_opts["title"]:
|
|
411
|
+
doc_opts["title"] = None
|
|
412
|
+
fonts = out.pop("fonts", None)
|
|
413
|
+
for k in _SIZE_KEYS:
|
|
414
|
+
if k in out:
|
|
415
|
+
raise ValueError(f"{k} is not an output option of figures (their size and "
|
|
416
|
+
"border are their width, height and margin settings)")
|
|
417
|
+
simplify = out.pop("simplify", None)
|
|
418
|
+
default_dpi = out.pop("default_dpi", None)
|
|
419
|
+
quality = output_quality(quality_of(fig, out), kind)
|
|
420
|
+
out.pop("quality", None)
|
|
421
|
+
if kind in _IMAGES:
|
|
422
|
+
cls = getattr(vec_io, kind.upper())
|
|
423
|
+
merged = _with_quality(out.get(kind), cls, kind, quality)
|
|
424
|
+
if merged is not None:
|
|
425
|
+
out[kind] = merged
|
|
426
|
+
else:
|
|
427
|
+
images = _with_quality(out.get("images"), vec_io.ImagePolicy, "images", quality)
|
|
428
|
+
if images is not None:
|
|
429
|
+
out["images"] = images
|
|
430
|
+
if simplify is not None:
|
|
431
|
+
simplify = out["simplify"] = simplify_value(simplify)
|
|
432
|
+
if out.get("dpi") is not None:
|
|
433
|
+
out["dpi"] = dpi(out["dpi"])
|
|
434
|
+
if out.get("flatten") is not None:
|
|
435
|
+
out["flatten"] = _flatten(out["flatten"])
|
|
436
|
+
fill = True
|
|
437
|
+
if kind in _IMAGES:
|
|
438
|
+
if out.get("dpi") is None:
|
|
439
|
+
out["dpi"] = dpi(default_dpi) if default_dpi is not None \
|
|
440
|
+
else fig.resolve("default_dpi")
|
|
441
|
+
from .color.maps import cycle_of, map_of
|
|
442
|
+
cycle = cycle_of(map_of(fig, "qualitative"))
|
|
443
|
+
if "background" in out: # in place of the figure's
|
|
444
|
+
fill = False
|
|
445
|
+
out["background"] = resolve_color(out["background"], cycle)
|
|
446
|
+
else:
|
|
447
|
+
out["background"] = resolve_color(fig.resolve("background"), cycle)
|
|
448
|
+
with _simplified(fig, simplify):
|
|
449
|
+
page = _page(fig, only, doc_opts, fill, fonts, (kind, out))
|
|
450
|
+
return page.document.write(kind, **out)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def save(fig: Any, file: Any, only: Any = None, format: str | None = None,
|
|
454
|
+
**options: Any) -> None:
|
|
455
|
+
"""Write the figure to a path or a binary file object, in ``format`` (default: the
|
|
456
|
+
path's extension)."""
|
|
457
|
+
if format is None:
|
|
458
|
+
if hasattr(file, "write"):
|
|
459
|
+
raise ValueError("give the format of a figure written to a file object")
|
|
460
|
+
ext = os.path.splitext(os.fspath(file))[1].lower().lstrip(".")
|
|
461
|
+
if ext not in FORMATS:
|
|
462
|
+
raise ValueError(f"unknown format of {os.fspath(file)!r}: use .pdf, .eps, .svg, "
|
|
463
|
+
".png, .jpg, .jxl or .avif, or give format=")
|
|
464
|
+
format = ext
|
|
465
|
+
data = write(fig, format, only, **options)
|
|
466
|
+
if hasattr(file, "write"):
|
|
467
|
+
file.write(data)
|
|
468
|
+
else:
|
|
469
|
+
with open(file, "wb") as f:
|
|
470
|
+
f.write(data)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def repr_svg(fig: Any, only: Any = None, include: Any = None,
|
|
474
|
+
exclude: Any = None) -> dict[str, str]:
|
|
475
|
+
"""The notebook bundle: ``{"image/svg+xml": svg}`` (as ``write("svg")``), or ``{}`` when
|
|
476
|
+
``include`` leaves the type out or ``exclude`` names it."""
|
|
477
|
+
kind = "image/svg+xml"
|
|
478
|
+
if (include is not None and kind not in include) or (exclude is not None and kind in exclude):
|
|
479
|
+
return {}
|
|
480
|
+
return {kind: write(fig, "svg", only).decode("utf-8")}
|