figworks 0.8.0__tar.gz
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.
- figworks-0.8.0/LICENSE +21 -0
- figworks-0.8.0/MANIFEST.in +3 -0
- figworks-0.8.0/PKG-INFO +431 -0
- figworks-0.8.0/README.md +394 -0
- figworks-0.8.0/pyproject.toml +83 -0
- figworks-0.8.0/setup.cfg +4 -0
- figworks-0.8.0/src/figworks/__init__.py +23 -0
- figworks-0.8.0/src/figworks/_export.py +34 -0
- figworks-0.8.0/src/figworks/collection.py +206 -0
- figworks-0.8.0/src/figworks/core/__init__.py +7 -0
- figworks-0.8.0/src/figworks/core/display.py +38 -0
- figworks-0.8.0/src/figworks/core/document.py +376 -0
- figworks-0.8.0/src/figworks/core/element.py +387 -0
- figworks-0.8.0/src/figworks/core/selectors.py +108 -0
- figworks-0.8.0/src/figworks/core/units.py +58 -0
- figworks-0.8.0/src/figworks/elements/__init__.py +7 -0
- figworks-0.8.0/src/figworks/elements/arrows.py +59 -0
- figworks-0.8.0/src/figworks/elements/shapes.py +91 -0
- figworks-0.8.0/src/figworks/elements/text.py +48 -0
- figworks-0.8.0/src/figworks/figure/__init__.py +8 -0
- figworks-0.8.0/src/figworks/figure/anchors.py +17 -0
- figworks-0.8.0/src/figworks/figure/figure.py +347 -0
- figworks-0.8.0/src/figworks/figure/grid.py +123 -0
- figworks-0.8.0/src/figworks/figure/layout.py +109 -0
- figworks-0.8.0/src/figworks/figure/panel.py +142 -0
- figworks-0.8.0/src/figworks/fonts.py +191 -0
- figworks-0.8.0/src/figworks/matplotlib.py +319 -0
- figworks-0.8.0/src/figworks/py.typed +0 -0
- figworks-0.8.0/src/figworks/style.py +306 -0
- figworks-0.8.0/src/figworks/styles/aps.md +45 -0
- figworks-0.8.0/src/figworks/styles/ieee.md +46 -0
- figworks-0.8.0/src/figworks/styles/nature.md +50 -0
- figworks-0.8.0/src/figworks/styles/paper.md +46 -0
- figworks-0.8.0/src/figworks/styles/presentation.md +20 -0
- figworks-0.8.0/src/figworks.egg-info/PKG-INFO +431 -0
- figworks-0.8.0/src/figworks.egg-info/SOURCES.txt +56 -0
- figworks-0.8.0/src/figworks.egg-info/dependency_links.txt +1 -0
- figworks-0.8.0/src/figworks.egg-info/requires.txt +14 -0
- figworks-0.8.0/src/figworks.egg-info/top_level.txt +1 -0
- figworks-0.8.0/tests/conftest.py +36 -0
- figworks-0.8.0/tests/test_align.py +147 -0
- figworks-0.8.0/tests/test_collection.py +135 -0
- figworks-0.8.0/tests/test_document.py +81 -0
- figworks-0.8.0/tests/test_export.py +57 -0
- figworks-0.8.0/tests/test_fonts.py +151 -0
- figworks-0.8.0/tests/test_grid.py +171 -0
- figworks-0.8.0/tests/test_ids.py +152 -0
- figworks-0.8.0/tests/test_matplotlib.py +67 -0
- figworks-0.8.0/tests/test_matplotlib_bridge.py +60 -0
- figworks-0.8.0/tests/test_placeholder.py +72 -0
- figworks-0.8.0/tests/test_placement.py +81 -0
- figworks-0.8.0/tests/test_selectors.py +55 -0
- figworks-0.8.0/tests/test_slots.py +117 -0
- figworks-0.8.0/tests/test_style.py +174 -0
- figworks-0.8.0/tests/test_units.py +10 -0
- figworks-0.8.0/tests/test_vectex.py +99 -0
- figworks-0.8.0/tests/test_vecview.py +318 -0
- figworks-0.8.0/tests/test_vecwire.py +109 -0
figworks-0.8.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrea Maiani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
figworks-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: figworks
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: A Python framework for building publication-quality scientific SVG figures.
|
|
5
|
+
Author: Andrea Maiani
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/maiani/figworks
|
|
8
|
+
Project-URL: Issues, https://github.com/maiani/figworks/issues
|
|
9
|
+
Keywords: svg,figures,matplotlib,scientific-visualization,publication
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: svg.py
|
|
24
|
+
Requires-Dist: lxml
|
|
25
|
+
Requires-Dist: matplotlib
|
|
26
|
+
Requires-Dist: CairoSVG
|
|
27
|
+
Requires-Dist: PyYAML>=6
|
|
28
|
+
Requires-Dist: vectex>=0.4
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: lxml-stubs>=0.5; extra == "dev"
|
|
31
|
+
Requires-Dist: ty>=0.0.84; extra == "dev"
|
|
32
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
35
|
+
Requires-Dist: zensical; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# FigWorks
|
|
39
|
+
|
|
40
|
+
FigWorks assembles multi-panel publication figures from Matplotlib plots, VecTeX
|
|
41
|
+
equations, VecView 3D scenes, VecWire circuit schematics, and native SVG
|
|
42
|
+
elements. The figure is generated from code and stays editable afterwards:
|
|
43
|
+
panels and elements keep stable ids, the same script writes byte-identical SVG,
|
|
44
|
+
and the result opens in Inkscape for final adjustments.
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/maiani/figworks/v0.8.0/docs/images/readme.svg" alt="A four-panel transmon figure: a 3D device with TeX labels, its circuit and Hamiltonian, the cosine potential with its levels, and the charge dispersion at two values of E_J/E_C" width="760">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<p align="center"><sub>A Physical Review two-column figure from <a href="https://github.com/maiani/figworks/blob/v0.8.0/examples/transmon_figure.py"><code>examples/transmon_figure.py</code></a>: a VecView device, a VecWire circuit, and two Matplotlib panels, labelled with VecTeX.</sub></p>
|
|
51
|
+
|
|
52
|
+
The four sources in that figure share nothing but a method, and FigWorks
|
|
53
|
+
composes them in a few lines:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
theme = load_theme("aps")
|
|
57
|
+
fig = Figure(width=theme.page["double"], height="104mm", theme=theme)
|
|
58
|
+
panels = fig.grid(
|
|
59
|
+
[["device", "device", "circuit"], ["potential", "bands", "bands"]],
|
|
60
|
+
height_ratios=(1.1, 1),
|
|
61
|
+
margins=("6mm", "3mm", "10mm", "12mm"), # top, right, bottom, left
|
|
62
|
+
gap=("13mm", "14mm"), # between rows, between columns
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
labels = {name: vectex.render(source, size_pt=8) for name, source in tex.items()}
|
|
66
|
+
|
|
67
|
+
panels["device"].add(device_scene(labels), id="device-scene") # a vecview.Scene
|
|
68
|
+
for name in ("EJ", "CB", "Cg", "res"):
|
|
69
|
+
fig.fill_slot(f"label-{name}", labels[name]) # pinned in 3D, still 8 pt
|
|
70
|
+
|
|
71
|
+
panels["circuit"].add(circuit_diagram(labels), id="circuit", fit="none") # at its own size
|
|
72
|
+
|
|
73
|
+
mpl_fig, ax = panels["potential"].subplots() # its axes frame is the panel
|
|
74
|
+
plot_potential(ax)
|
|
75
|
+
panels["potential"].add(mpl_fig, id="potential-plot", fit="axes")
|
|
76
|
+
|
|
77
|
+
mpl_fig, axes = panels["bands"].subplots(1, 2, sharey=True, gridspec_kw=dict(wspace=0.12))
|
|
78
|
+
plot_bands(axes)
|
|
79
|
+
panels["bands"].add(mpl_fig, id="bands-plot", fit="axes")
|
|
80
|
+
|
|
81
|
+
for letter, panel in zip("abcd", panels.values(), strict=True):
|
|
82
|
+
fig.label(letter, panel.nw)
|
|
83
|
+
fig.save("transmon_figure.pdf")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`device_scene` returns a `vecview.Scene`, `circuit_diagram` a `vecwire.Circuit`,
|
|
87
|
+
and the plot functions are plain Matplotlib, all in the example. The grid fixes
|
|
88
|
+
the panels in millimetres, the plots' frames land exactly on them, the circuit
|
|
89
|
+
is placed at the size it was drawn, so every label prints at the style's 8 pt,
|
|
90
|
+
and the export refuses to substitute a missing font.
|
|
91
|
+
|
|
92
|
+
SVG is the canonical output; PDF and PNG are exported through CairoSVG.
|
|
93
|
+
FigWorks is an assembly layer between plotting code and the final graphic. It
|
|
94
|
+
does not replace Matplotlib, TeX, Inkscape, or Illustrator.
|
|
95
|
+
|
|
96
|
+
FigWorks is alpha: the core figure model works and is tested, but the API is
|
|
97
|
+
still settling and a minor release may change it.
|
|
98
|
+
|
|
99
|
+
## Install
|
|
100
|
+
|
|
101
|
+
FigWorks is on PyPI and needs Python 3.12 or newer:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m pip install figworks
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This pulls in Matplotlib, lxml, svg.py, CairoSVG, and VecTeX. Rendering
|
|
108
|
+
equations with VecTeX also needs a TeX installation with `pdflatex` and
|
|
109
|
+
`dvisvgm` on `PATH`, and MuPDF's `mutool` with a current Ghostscript
|
|
110
|
+
(`mupdf-tools` on Debian and Ubuntu). [VecView](https://github.com/maiani/vecview)
|
|
111
|
+
and [VecWire](https://github.com/maiani/vecwire) are optional. VecView is on
|
|
112
|
+
PyPI; VecWire is installed from GitHub:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python -m pip install "vecview>=0.3"
|
|
116
|
+
python -m pip install "vecwire @ git+https://github.com/maiani/vecwire"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Figures are set in TeX Gyre Heros by default, a free Helvetica clone that comes
|
|
120
|
+
with TeX Live and as `fonts-texgyre` on Debian and Ubuntu. PDF and PNG export
|
|
121
|
+
check, through fontconfig, that every font a figure uses is installed and has
|
|
122
|
+
every glyph it needs, and raise `FontError` rather than substitute silently.
|
|
123
|
+
|
|
124
|
+
## Quick start
|
|
125
|
+
|
|
126
|
+
Two plots in one Physical Review column, needing only Matplotlib:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import matplotlib.pyplot as plt
|
|
130
|
+
import numpy as np
|
|
131
|
+
|
|
132
|
+
from figworks import Figure
|
|
133
|
+
from figworks.matplotlib import theme_rc
|
|
134
|
+
|
|
135
|
+
plt.rcParams.update(theme_rc("aps")) # Matplotlib in the figure's face and sizes
|
|
136
|
+
|
|
137
|
+
fig = Figure(width="86mm", height="42mm", theme="aps") # one Physical Review column
|
|
138
|
+
panels = fig.grid([["power", "phase"]], margins=("5mm", "2mm", "10mm", "13mm"), gap="15mm")
|
|
139
|
+
|
|
140
|
+
detuning = np.linspace(-4, 4, 200)
|
|
141
|
+
response = 1 / (1 + 2j * detuning)
|
|
142
|
+
for name, curve, ylabel in (
|
|
143
|
+
("power", abs(response) ** 2, r"$|S_{21}|^2$"),
|
|
144
|
+
("phase", np.angle(response), r"$\arg S_{21}$ (rad)"),
|
|
145
|
+
):
|
|
146
|
+
mpl_fig, ax = panels[name].subplots() # its axes frame is exactly the panel
|
|
147
|
+
(line,) = ax.plot(detuning, curve)
|
|
148
|
+
line.set_gid(f"{name}-curve") # survives into the SVG, for selectors and Inkscape
|
|
149
|
+
ax.set_xlabel(r"$\Delta/\kappa$")
|
|
150
|
+
ax.set_ylabel(ylabel)
|
|
151
|
+
panels[name].add(mpl_fig, id=f"{name}-plot", fit="axes")
|
|
152
|
+
|
|
153
|
+
fig.label("a", panels["power"].nw)
|
|
154
|
+
fig.label("b", panels["phase"].nw)
|
|
155
|
+
fig.select("#phase-curve").set_style(stroke="#c2185b") # edit by id after composing
|
|
156
|
+
fig.save("resonance.svg")
|
|
157
|
+
fig.save("resonance.pdf")
|
|
158
|
+
fig.save("resonance.png", dpi=600)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The two frames are the same size and sit level, however wide their tick labels.
|
|
162
|
+
Coordinates and sizes accept `px`, `pt`, `mm`, `cm`, or `in`; bare numbers are
|
|
163
|
+
px. The curve ids survive composition, so a selector reaches the phase curve
|
|
164
|
+
inside its placed plot, and Inkscape finds it by the same id.
|
|
165
|
+
|
|
166
|
+
For the common case of dropping SVG into existing Matplotlib axes, `compose`
|
|
167
|
+
needs no `Figure` at all:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import figworks
|
|
171
|
+
import matplotlib.pyplot as plt
|
|
172
|
+
|
|
173
|
+
fig, ax = plt.subplots()
|
|
174
|
+
ax.plot([0, 1], [0, 1])
|
|
175
|
+
svg = figworks.compose({ax: "logo.svg"}, fig=fig)
|
|
176
|
+
with open("figure.svg", "w", encoding="utf-8") as output:
|
|
177
|
+
output.write(svg)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## What it does
|
|
181
|
+
|
|
182
|
+
The root package exports `Figure`, `Panel`, `Anchor`, `Theme`, `compose`,
|
|
183
|
+
`layout_svgs`, `display_svg`, and `FigureCollection`. With them you can:
|
|
184
|
+
|
|
185
|
+
- create an SVG canvas with physical dimensions and a style: `"paper"`,
|
|
186
|
+
`"presentation"`, the journal styles `"nature"`, `"aps"`, and `"ieee"`, or
|
|
187
|
+
your own `style.md`;
|
|
188
|
+
- divide it into named panels with `Figure.grid`, or place panels by hand;
|
|
189
|
+
- add rectangular panels with named anchors (`panel.nw`, `panel.center`, …);
|
|
190
|
+
- place Matplotlib figures, VecTeX fragments, VecView scenes, VecWire circuits,
|
|
191
|
+
SVG files, and SVG strings through one call, `Panel.add`;
|
|
192
|
+
- add native text, panel labels, rectangles, lines, circles, ellipses,
|
|
193
|
+
polylines, paths, and arrows;
|
|
194
|
+
- reserve placeholders and fill them by id later;
|
|
195
|
+
- lay out many SVGs into a labelled grid;
|
|
196
|
+
- select elements by `#id`, `.class`, or tag name, then delete them or set
|
|
197
|
+
attributes and inline style, which reaches the leaves a source styled itself;
|
|
198
|
+
- save to `.svg`, `.pdf`, and `.png`, byte-identically from run to run, and
|
|
199
|
+
display figures inline in notebooks;
|
|
200
|
+
- regenerate a whole set of figures from one command with `FigureCollection`.
|
|
201
|
+
|
|
202
|
+
`Panel.add` reads each source's intrinsic size from its `viewBox`, scales it
|
|
203
|
+
uniformly, and centres it in the panel; `fit="none"` keeps the size the source
|
|
204
|
+
declares instead, so an 8 pt label prints at 8 pt, and `align` picks the point
|
|
205
|
+
of the panel it sits on. Placement wraps the source in a group
|
|
206
|
+
and keeps its ids, prefixing only those that collide with ids already in the
|
|
207
|
+
figure, so selectors reach inside placed content.
|
|
208
|
+
|
|
209
|
+
## Sources
|
|
210
|
+
|
|
211
|
+
Everything except Matplotlib integrates through one method: FigWorks places any
|
|
212
|
+
object exposing `to_svg_document()`. VecTeX, VecView, and VecWire implement it
|
|
213
|
+
without importing FigWorks, and FigWorks has no adapter for any of them.
|
|
214
|
+
|
|
215
|
+
### VecTeX equations
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import vectex
|
|
219
|
+
|
|
220
|
+
equation = vectex.render(r"\[E = mc^2\]", id_prefix="equation")
|
|
221
|
+
panel.add(equation, id="equation-panel")
|
|
222
|
+
fig.select("#equation-root").set_attr("color", "navy") # VecTeX glyphs use currentColor
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### VecView scenes
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
import vecview
|
|
229
|
+
|
|
230
|
+
cam = vecview.OrthographicCamera(azim_deg=35, elev_deg=24, scale=62)
|
|
231
|
+
scene = vecview.Scene(cam, pad=6)
|
|
232
|
+
slab = vecview.box_faces(center=(0, 0, -0.45), size=(11, 9, 0.9))
|
|
233
|
+
scene.faces(10, slab, cull=True, fill="#cfd6e0")
|
|
234
|
+
|
|
235
|
+
panel.add(scene, id="slab-scene")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Leave the scene's `background` unset and keep `pad` small: the scene is scaled
|
|
239
|
+
to fit its panel, so padding shrinks the drawing.
|
|
240
|
+
|
|
241
|
+
A scene can also hold content of its own. `Scene.plane` reserves a rectangle of
|
|
242
|
+
a world plane and `Figure.fill_plane` puts a plot *in* it, foreshortened with
|
|
243
|
+
the geometry. `Scene.slot` pins an empty, upright group to a world point and
|
|
244
|
+
`Figure.fill_slot` fills it with content kept at its own physical size, so an
|
|
245
|
+
8 pt TeX label stays 8 pt however the scene was scaled:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
scene.plane(15, origin=(-4.2, -2.9, 0.01), u_edge=(0, 8.4, 0), v_edge=(5.8, 0, 0), id="plot-plane")
|
|
249
|
+
|
|
250
|
+
label = vectex.render(r"$\hat{z}$", size_pt=8)
|
|
251
|
+
scene.slot(45, (5.5, 4.5, 0), label.width_px, label.height_px, align="west", dx=1.6, id="label-z")
|
|
252
|
+
|
|
253
|
+
panel.add(scene, id="device")
|
|
254
|
+
fig.fill_plane("plot-plane", mpl_fig)
|
|
255
|
+
fig.fill_slot("label-z", label)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`fill_plane` normalizes content onto the unit square, so shape the plane to the
|
|
259
|
+
plot's aspect ratio. [3D scenes](https://github.com/maiani/figworks/blob/v0.8.0/docs/usage/scenes-3d.md) covers orientation,
|
|
260
|
+
line weights, and export caveats.
|
|
261
|
+
|
|
262
|
+
### VecWire circuits
|
|
263
|
+
|
|
264
|
+
A `vecwire.Circuit` places like any other source, and its component ids and
|
|
265
|
+
`data-component` attributes survive placement:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
right = fig.panel("schematic", x="120mm", y="8mm", w="40mm", h="56mm")
|
|
269
|
+
right.add(circuit, id="transmon-circuit")
|
|
270
|
+
fig.select("#JJ") # a junction drawn with id="JJ"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### One style document per publication
|
|
274
|
+
|
|
275
|
+
```markdown
|
|
276
|
+
---
|
|
277
|
+
base: aps # or nature, ieee, paper, presentation, another style.md
|
|
278
|
+
color:
|
|
279
|
+
spin_up: "#d62828"
|
|
280
|
+
cycle: [spin_up]
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
# Style guide: what each colour means, which weight is for what ...
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
A `style.md` holds the publication's design tokens in its YAML frontmatter and
|
|
287
|
+
its style guide in prose. FigWorks generates Matplotlib settings from it
|
|
288
|
+
(`theme_rc("style.md")`), styles its own elements with it
|
|
289
|
+
(`Figure(..., theme="style.md")`), and builds figure sets under it
|
|
290
|
+
(`FigureCollection(style=...)`). Built-in bases follow the figure guidelines of
|
|
291
|
+
Nature, Physical Review, and IEEE. See [Styles](https://github.com/maiani/figworks/blob/v0.8.0/docs/usage/styles.md).
|
|
292
|
+
|
|
293
|
+
### Aligned Matplotlib panels
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
panel = fig.panel("a", x="18mm", y="8mm", w="62mm", h="48mm")
|
|
297
|
+
mpl_fig, ax = panel.subplots() # axes frame exactly the panel, at 1:1
|
|
298
|
+
ax.plot(x, y)
|
|
299
|
+
panel.add(mpl_fig, id="plot-a", fit="axes")
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Plots made this way keep their nominal text size, and their axes frames sit
|
|
303
|
+
exactly on their panels, so frames in a row line up whatever their tick labels.
|
|
304
|
+
|
|
305
|
+
### Matplotlib, both directions
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from figworks.matplotlib import connect, insert, mpl_to_svg
|
|
309
|
+
|
|
310
|
+
svg = mpl_to_svg(mpl_fig) # Matplotlib -> SVG
|
|
311
|
+
connect(ax, "slot", 0.2, 0.2, 0.6, 0.6) # reserve a placeholder in the axes
|
|
312
|
+
result = insert({"slot": panel_fig}, fig=mpl_fig) # swap in vector SVG by id
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Matplotlib exports are made deterministic: FigWorks pins the id salt and drops
|
|
316
|
+
the export date, which otherwise change on every run.
|
|
317
|
+
|
|
318
|
+
## Placeholders and grids
|
|
319
|
+
|
|
320
|
+
Create named panels without calculating their coordinates:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
fig = Figure(width="180mm", height="110mm")
|
|
324
|
+
panels = fig.grid(
|
|
325
|
+
[["device", "device"], ["spectrum", "response"]],
|
|
326
|
+
width_ratios=(3, 2),
|
|
327
|
+
height_ratios=(1, 2),
|
|
328
|
+
margins=("8mm", "6mm", "12mm", "16mm"), # top, right, bottom, left
|
|
329
|
+
gap=("16mm", "22mm"), # between rows, between columns
|
|
330
|
+
)
|
|
331
|
+
panels["device"].add(scene, id="device-scene")
|
|
332
|
+
mpl_fig, ax = panels["spectrum"].subplots()
|
|
333
|
+
ax.plot(x, y)
|
|
334
|
+
panels["spectrum"].add(mpl_fig, id="spectrum-plot", fit="axes")
|
|
335
|
+
fig.label("b", panels["spectrum"].nw)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Repeating a name spans a rectangle; `None` reserves an empty cell. Ratios
|
|
339
|
+
divide the space remaining after margins and gaps, which keep their physical
|
|
340
|
+
size when you build a figure at another width. The returned dictionary holds
|
|
341
|
+
ordinary `Panel` objects in first-occurrence order. See [Grid layout](https://github.com/maiani/figworks/blob/v0.8.0/docs/usage/layout.md)
|
|
342
|
+
and [the complete example](https://github.com/maiani/figworks/blob/v0.8.0/examples/panel_grid.py).
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
fig.placeholder("main-slot", x="10mm", y="10mm", w="80mm", h="60mm", label="x")
|
|
346
|
+
fig.fill("main-slot", mpl_fig) # a figure, an SVG file path, or an SVG string
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from figworks import layout_svgs
|
|
351
|
+
|
|
352
|
+
grid = layout_svgs([svg_a, svg_b, svg_c], labels=["a", "b", "c"], outline=True)
|
|
353
|
+
grid.save("grid.svg")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Known limitations
|
|
357
|
+
|
|
358
|
+
A gradient-filled `<mask>` does not survive CairoSVG and vanishes from PDF and
|
|
359
|
+
PNG exports without a warning.
|
|
360
|
+
|
|
361
|
+
## Examples
|
|
362
|
+
|
|
363
|
+
Run from the repository root:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
python examples/transmon_figure.py # requires vecview, vecwire, and TeX
|
|
367
|
+
python examples/minimal_svg.py
|
|
368
|
+
python examples/matplotlib_panel.py
|
|
369
|
+
python examples/two_panel_figure.py
|
|
370
|
+
python examples/panel_grid.py
|
|
371
|
+
python examples/matplotlib_bridge.py
|
|
372
|
+
python examples/vecview_panel.py # requires vecview
|
|
373
|
+
python examples/vecview_plane_plot.py # requires vecview; a plot in a 3D plane
|
|
374
|
+
python examples/vecwire_panel.py # requires vecwire
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Each example writes its figures into `examples/out/`.
|
|
378
|
+
|
|
379
|
+
## The suite
|
|
380
|
+
|
|
381
|
+
FigWorks is the composition layer of four projects developed together, each
|
|
382
|
+
independently useful:
|
|
383
|
+
|
|
384
|
+
| Project | Produces | Maturity |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| **FigWorks** | composed, exported multi-panel figures | alpha: core API still settling |
|
|
387
|
+
| [VecTeX](https://github.com/maiani/vectex) | editable TeX equations as SVG fragments | beta: on PyPI, API settled enough to build on |
|
|
388
|
+
| [VecView](https://github.com/maiani/vecview) | layered 3D schematics as SVG documents | alpha: on PyPI, API may change before 1.0 |
|
|
389
|
+
| [VecWire](https://github.com/maiani/vecwire) | editable circuit schematics as SVG documents | pre-alpha: first version, on GitHub, not yet on PyPI |
|
|
390
|
+
|
|
391
|
+
All four emit vector SVG with stable ids and byte-identical output for
|
|
392
|
+
identical input, so a figure can be regenerated from code, diffed in version
|
|
393
|
+
control, and still hand-tuned in Inkscape. All four are pre-1.0 and make no
|
|
394
|
+
backward-compatibility promise; deterministic output is the one guarantee they
|
|
395
|
+
share.
|
|
396
|
+
|
|
397
|
+
The dependency edges are uneven by design: VecTeX is a runtime requirement,
|
|
398
|
+
VecView and VecWire are optional, and none of the three depends on FigWorks.
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
## Development
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
python -m venv .venv
|
|
405
|
+
source .venv/bin/activate
|
|
406
|
+
python -m pip install -e ".[dev]"
|
|
407
|
+
ruff format --check .
|
|
408
|
+
ruff check .
|
|
409
|
+
ty check
|
|
410
|
+
pytest
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Tests for VecView and VecWire skip when those packages are not installed, and
|
|
414
|
+
tests that render VecTeX skip without `pdflatex` and `dvisvgm`.
|
|
415
|
+
Rendering tests need TeX Gyre Heros, DejaVu Sans, and fontconfig's command-line
|
|
416
|
+
tools (Debian/Ubuntu: `fonts-texgyre fonts-dejavu-core fontconfig`).
|
|
417
|
+
|
|
418
|
+
## Documentation
|
|
419
|
+
|
|
420
|
+
The docs in `docs/` are built with [Zensical](https://zensical.org):
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
zensical serve # preview at http://localhost:8000
|
|
424
|
+
zensical build # static build into site/
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
MIT
|