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.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. 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")}