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.
Files changed (58) hide show
  1. figworks-0.8.0/LICENSE +21 -0
  2. figworks-0.8.0/MANIFEST.in +3 -0
  3. figworks-0.8.0/PKG-INFO +431 -0
  4. figworks-0.8.0/README.md +394 -0
  5. figworks-0.8.0/pyproject.toml +83 -0
  6. figworks-0.8.0/setup.cfg +4 -0
  7. figworks-0.8.0/src/figworks/__init__.py +23 -0
  8. figworks-0.8.0/src/figworks/_export.py +34 -0
  9. figworks-0.8.0/src/figworks/collection.py +206 -0
  10. figworks-0.8.0/src/figworks/core/__init__.py +7 -0
  11. figworks-0.8.0/src/figworks/core/display.py +38 -0
  12. figworks-0.8.0/src/figworks/core/document.py +376 -0
  13. figworks-0.8.0/src/figworks/core/element.py +387 -0
  14. figworks-0.8.0/src/figworks/core/selectors.py +108 -0
  15. figworks-0.8.0/src/figworks/core/units.py +58 -0
  16. figworks-0.8.0/src/figworks/elements/__init__.py +7 -0
  17. figworks-0.8.0/src/figworks/elements/arrows.py +59 -0
  18. figworks-0.8.0/src/figworks/elements/shapes.py +91 -0
  19. figworks-0.8.0/src/figworks/elements/text.py +48 -0
  20. figworks-0.8.0/src/figworks/figure/__init__.py +8 -0
  21. figworks-0.8.0/src/figworks/figure/anchors.py +17 -0
  22. figworks-0.8.0/src/figworks/figure/figure.py +347 -0
  23. figworks-0.8.0/src/figworks/figure/grid.py +123 -0
  24. figworks-0.8.0/src/figworks/figure/layout.py +109 -0
  25. figworks-0.8.0/src/figworks/figure/panel.py +142 -0
  26. figworks-0.8.0/src/figworks/fonts.py +191 -0
  27. figworks-0.8.0/src/figworks/matplotlib.py +319 -0
  28. figworks-0.8.0/src/figworks/py.typed +0 -0
  29. figworks-0.8.0/src/figworks/style.py +306 -0
  30. figworks-0.8.0/src/figworks/styles/aps.md +45 -0
  31. figworks-0.8.0/src/figworks/styles/ieee.md +46 -0
  32. figworks-0.8.0/src/figworks/styles/nature.md +50 -0
  33. figworks-0.8.0/src/figworks/styles/paper.md +46 -0
  34. figworks-0.8.0/src/figworks/styles/presentation.md +20 -0
  35. figworks-0.8.0/src/figworks.egg-info/PKG-INFO +431 -0
  36. figworks-0.8.0/src/figworks.egg-info/SOURCES.txt +56 -0
  37. figworks-0.8.0/src/figworks.egg-info/dependency_links.txt +1 -0
  38. figworks-0.8.0/src/figworks.egg-info/requires.txt +14 -0
  39. figworks-0.8.0/src/figworks.egg-info/top_level.txt +1 -0
  40. figworks-0.8.0/tests/conftest.py +36 -0
  41. figworks-0.8.0/tests/test_align.py +147 -0
  42. figworks-0.8.0/tests/test_collection.py +135 -0
  43. figworks-0.8.0/tests/test_document.py +81 -0
  44. figworks-0.8.0/tests/test_export.py +57 -0
  45. figworks-0.8.0/tests/test_fonts.py +151 -0
  46. figworks-0.8.0/tests/test_grid.py +171 -0
  47. figworks-0.8.0/tests/test_ids.py +152 -0
  48. figworks-0.8.0/tests/test_matplotlib.py +67 -0
  49. figworks-0.8.0/tests/test_matplotlib_bridge.py +60 -0
  50. figworks-0.8.0/tests/test_placeholder.py +72 -0
  51. figworks-0.8.0/tests/test_placement.py +81 -0
  52. figworks-0.8.0/tests/test_selectors.py +55 -0
  53. figworks-0.8.0/tests/test_slots.py +117 -0
  54. figworks-0.8.0/tests/test_style.py +174 -0
  55. figworks-0.8.0/tests/test_units.py +10 -0
  56. figworks-0.8.0/tests/test_vectex.py +99 -0
  57. figworks-0.8.0/tests/test_vecview.py +318 -0
  58. 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.
@@ -0,0 +1,3 @@
1
+ # The sdist ships the test suite; conftest.py holds fixtures it needs.
2
+ graft tests
3
+ global-exclude __pycache__ *.py[cod]
@@ -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