Skip to main content

FigWorks

CI PyPI Python License: MIT

A Python suite for reproducible, reviewable, and editable scientific figures.

FigWorks brings together Matplotlib and Plotly plots, VecTeX equations, VecView 3D scenes, VecWire circuit schematics, and native SVG annotations on a physical page. Each producer is independently useful; FigWorks composes their output into a publication figure.

  • Reproducible: regenerate figures from source, with deterministic SVG for identical inputs in the same toolchain and font environment.
  • Reviewable: inspect the Python, styling, and semantic SVG in version control. Named panels and elements make changes traceable.
  • Editable: keep vector paths, text, equations, and groups available for final adjustments in Inkscape. VecTeX retains TeX source for TexText editing.

Figures you own, even when AI helped make them. An assistant can draft the script, but what you keep is plain Python in your repository and SVG with named parts: every number traces back to code you can check, every label can be changed by hand, and the figure rebuilds from source whenever you need it.

Documentation · Quick start · Examples · The suite · Report an issue

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

A Physical Review two-column figure from examples/transmon_figure.py: a VecView device, a VecWire circuit, and two Matplotlib panels, labelled with VecTeX.

The four sources in that figure share nothing but a method, and FigWorks composes them in a few lines:

theme = load_theme("aps")
fig = Figure(width=theme.page["double"], height="104mm", theme=theme)
panels = fig.grid(
    [["device", "device", "circuit"], ["potential", "bands", "bands"]],
    height_ratios=(1.1, 1),
    margins=("6mm", "3mm", "10mm", "12mm"),  # top, right, bottom, left
    gap=("13mm", "14mm"),  # between rows, between columns
)

labels = {name: vectex.render(source, size_pt=8) for name, source in tex.items()}

panels["device"].add(device_scene(labels), id="device-scene")  # labels pinned in 3D, still 8 pt

panels["circuit"].add(circuit_diagram(labels), id="circuit", fit="none")  # at its own size

mpl_fig, ax = panels["potential"].subplots()  # its axes frame is the panel
plot_potential(ax)
panels["potential"].add(mpl_fig, id="potential-plot", fit="axes")

mpl_fig, axes = panels["bands"].subplots(1, 2, sharey=True, gridspec_kw=dict(wspace=0.12))
plot_bands(axes)
panels["bands"].add(mpl_fig, id="bands-plot", fit="axes")

for letter, panel in zip("abcd", panels.values(), strict=True):
    fig.label(letter, panel.nw)
fig.save("transmon_figure.pdf")

device_scene returns a vecview.Scene, circuit_diagram a vecwire.Circuit, and the plot functions are plain Matplotlib, all in the example. The grid fixes the panels in millimetres, the plots' frames land exactly on them, the circuit is placed at the size it was drawn, so every label prints at the style's 8 pt, and the export refuses to substitute a missing font.

SVG is the canonical output; PDF and PNG are exported through CairoSVG. FigWorks is an assembly layer between plotting code and the final graphic. It does not replace Matplotlib, TeX, Inkscape, or Illustrator.

FigWorks is alpha: the core figure model works and is tested, but the API is still settling and a minor release may change it.

Install

FigWorks is on PyPI and needs Python 3.12 or newer:

python -m pip install figworks

This pulls in Matplotlib, lxml, svg.py, CairoSVG, and VecTeX. Rendering equations with VecTeX also needs a TeX installation with pdflatex and dvisvgm on PATH, and MuPDF's mutool with a current Ghostscript (mupdf-tools on Debian and Ubuntu). VecView and VecWire are optional, both on PyPI:

python -m pip install "vecview>=0.4" "vecwire>=0.3"

Figures are set in TeX Gyre Heros by default, a free Helvetica clone that comes with TeX Live and as fonts-texgyre on Debian and Ubuntu. PDF and PNG export check, through fontconfig, that every font a figure uses is installed and has every glyph it needs, and raise FontError rather than substitute silently.

Quick start

Two plots in one Physical Review column, needing only Matplotlib:

import matplotlib.pyplot as plt
import numpy as np

from figworks import Figure
from figworks.matplotlib import theme_rc

plt.rcParams.update(theme_rc("aps"))  # Matplotlib in the figure's face and sizes

fig = Figure(width="86mm", height="42mm", theme="aps")  # one Physical Review column
panels = fig.grid([["power", "phase"]], margins=("5mm", "2mm", "10mm", "13mm"), gap="15mm")

detuning = np.linspace(-4, 4, 200)
response = 1 / (1 + 2j * detuning)
for name, curve, ylabel in (
    ("power", abs(response) ** 2, r"$|S_{21}|^2$"),
    ("phase", np.angle(response), r"$\arg S_{21}$ (rad)"),
):
    mpl_fig, ax = panels[name].subplots()  # its axes frame is exactly the panel
    (line,) = ax.plot(detuning, curve)
    line.set_gid(f"{name}-curve")  # survives into the SVG, for selectors and Inkscape
    ax.set_xlabel(r"$\Delta/\kappa$")
    ax.set_ylabel(ylabel)
    panels[name].add(mpl_fig, id=f"{name}-plot", fit="axes")

fig.label("a", panels["power"].nw)
fig.label("b", panels["phase"].nw)
fig.select("#phase-curve").set_style(stroke="#c2185b")  # edit by id after composing
fig.save("resonance.svg")
fig.save("resonance.pdf")
fig.save("resonance.png", dpi=600)

The two frames are the same size and sit level, however wide their tick labels. Coordinates and sizes accept px, pt, mm, cm, or in; bare numbers are px. The curve ids survive composition, so a selector reaches the phase curve inside its placed plot, and Inkscape finds it by the same id.

For the common case of dropping SVG into existing Matplotlib axes, compose needs no Figure at all:

import figworks
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
svg = figworks.compose({ax: "logo.svg"}, fig=fig)
with open("figure.svg", "w", encoding="utf-8") as output:
    output.write(svg)

What it does

The root package exports Figure, Panel, Anchor, Theme, compose, layout_svgs, display_svg, and FigureCollection. With them you can:

  • create an SVG canvas with physical dimensions and a style: "paper", "presentation", the journal styles "nature", "aps", and "ieee", or your own style.md;
  • divide it into named panels with Figure.grid, or place panels by hand;
  • add rectangular panels with named anchors (panel.nw, panel.center, …);
  • place Matplotlib figures, VecTeX fragments, VecView scenes, VecWire circuits, SVG files, and SVG strings through one call, Panel.add;
  • add native text, panel labels, rectangles, lines, circles, ellipses, polylines, paths, and arrows;
  • reserve placeholders and fill them by id later;
  • lay out many SVGs into a labelled grid;
  • select elements by #id, .class, or tag name, then delete them or set attributes and inline style, which reaches the leaves a source styled itself;
  • save to .svg, .pdf, and .png, byte-identically from run to run, and display figures inline in notebooks;
  • regenerate a whole set of figures from one command with FigureCollection.

Panel.add reads each source's intrinsic size from its viewBox, scales it uniformly, and centres it in the panel; fit="none" keeps the size the source declares instead, so an 8 pt label prints at 8 pt, and align picks the point of the panel it sits on. Placement wraps the source in a group and keeps its ids, prefixing only those that collide with ids already in the figure, so selectors reach inside placed content.

Sources

SVG-producing objects integrate through one method: FigWorks places any object exposing to_svg_document(). VecTeX, VecView, and VecWire implement it without importing FigWorks, and FigWorks has no adapter for any of them.

Plotly plots

Install pip install "figworks[plotly]" and Chrome/Chromium, then pass plotly_to_svg(plot) from figworks.plotly to Panel.add or Figure.add. This produces static SVG; WebGL traces may include raster images. See the Plotly guide for dimensions, fonts, reproducibility, and an example.

VecTeX equations

import vectex

equation = vectex.render(r"\[E = mc^2\]", id_prefix="equation")
panel.add(equation, id="equation-panel")
fig.select("#equation-root").set_attr("color", "navy")  # VecTeX glyphs use currentColor

VecView scenes

import vecview

cam = vecview.OrthographicCamera(azim_deg=35, elev_deg=24, scale=62)
scene = vecview.Scene(cam, pad=6)
slab = vecview.box_faces(center=(0, 0, -0.45), size=(11, 9, 0.9))
scene.faces(10, slab, cull=True, fill="#cfd6e0")

panel.add(scene, id="slab-scene")

Leave the scene's background unset and keep pad small: the scene is scaled to fit its panel, so padding shrinks the drawing.

A scene can also hold content of its own. Scene.plane reserves a rectangle of a world plane and Figure.fill_plane puts a plot in it, foreshortened with the geometry. Scene.slot pins upright content to a world point, and FigWorks keeps that content at its own size, so an 8 pt TeX label stays 8 pt however the scene was scaled:

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")

label = vectex.render(r"$\hat{z}$", size_pt=8)
w, h = label.width_px, label.height_px
scene.slot(45, (5.5, 4.5, 0), w, h, content=label.to_svg_py(), align="west", dx=1.6, id="label-z")

panel.add(scene, id="device")
fig.fill_plane("plot-plane", mpl_fig)

fill_plane normalizes content onto the unit square, so shape the plane to the plot's aspect ratio. 3D scenes covers orientation, line weights, and export caveats.

VecWire circuits

A vecwire.Circuit places like any other source, and its component ids and data-component attributes survive placement:

right = fig.panel("schematic", x="120mm", y="8mm", w="40mm", h="56mm")
right.add(circuit, id="transmon-circuit")
fig.select("#JJ")  # a junction drawn with id="JJ"

One style document per publication

---
base: aps            # or nature, ieee, paper, presentation, another style.md
color:
  spin_up: "#d62828"
cycle: [spin_up]
---

# Style guide: what each colour means, which weight is for what ...

A style.md holds the publication's design tokens in its YAML frontmatter and its style guide in prose. FigWorks generates Matplotlib settings from it (theme_rc("style.md")), styles its own elements with it (Figure(..., theme="style.md")), and builds figure sets under it (FigureCollection(style=...)). Built-in bases follow the figure guidelines of Nature, Physical Review, and IEEE. See Styles.

One typography across the suite

In the development checkout, a loaded theme supplies ordinary keyword arguments for each producer, with the correct physical units:

theme = load_theme("aps")
plt.rcParams.update(theme.rc())
plotly_figure.update_layout(template=theme.plotly())
equation = vectex.render(r"$E = mc^2$", **theme.vectex())
scene.text(20, (0, 0, 1), "Device", **theme.vecview_text())
circuit = Circuit(100, 60, **theme.vecwire())
circuit.label("C", (40, 20), **theme.vecwire_label())

Use fit="none" to retain the source's physical type size; fitting a whole source to a smaller panel also shrinks its labels and strokes. The helper for VecWire labels requires its current checkout with font_family support. See the style guide and examples/transmon_figure.py for a complete mixed-source figure.

Aligned Matplotlib panels

panel = fig.panel("a", x="18mm", y="8mm", w="62mm", h="48mm")
mpl_fig, ax = panel.subplots()  # axes frame exactly the panel, at 1:1
ax.plot(x, y)
panel.add(mpl_fig, id="plot-a", fit="axes")

Plots made this way keep their nominal text size, and their axes frames sit exactly on their panels, so frames in a row line up whatever their tick labels.

Matplotlib, both directions

from figworks.matplotlib import connect, insert, mpl_to_svg

svg = mpl_to_svg(mpl_fig)  # Matplotlib -> SVG
connect(ax, "slot", 0.2, 0.2, 0.6, 0.6)  # reserve a placeholder in the axes
result = insert({"slot": panel_fig}, fig=mpl_fig)  # swap in vector SVG by id

Matplotlib exports are made deterministic: FigWorks pins the id salt and drops the export date, which otherwise change on every run.

Placeholders and grids

Create named panels without calculating their coordinates:

fig = Figure(width="180mm", height="110mm")
panels = fig.grid(
    [["device", "device"], ["spectrum", "response"]],
    width_ratios=(3, 2),
    height_ratios=(1, 2),
    margins=("8mm", "6mm", "12mm", "16mm"),  # top, right, bottom, left
    gap=("16mm", "22mm"),  # between rows, between columns
)
panels["device"].add(scene, id="device-scene")
mpl_fig, ax = panels["spectrum"].subplots()
ax.plot(x, y)
panels["spectrum"].add(mpl_fig, id="spectrum-plot", fit="axes")
fig.label("b", panels["spectrum"].nw)

Repeating a name spans a rectangle; None reserves an empty cell. Ratios divide the space remaining after margins and gaps, which keep their physical size when you build a figure at another width. The returned dictionary holds ordinary Panel objects in first-occurrence order. See Grid layout and the complete example.

fig.placeholder("main-slot", x="10mm", y="10mm", w="80mm", h="60mm", label="x")
fig.fill("main-slot", mpl_fig)  # a figure, an SVG file path, or an SVG string
from figworks import layout_svgs

grid = layout_svgs([svg_a, svg_b, svg_c], labels=["a", "b", "c"], outline=True)
grid.save("grid.svg")

Publication preflight

Inspect the assembled figure before exporting:

report = figure.check(target="pdf")
for issue in report.issues:
    print(issue.severity, issue.code, issue.message)
report.raise_for_errors()

The checker reports broken IDs/references, font problems, small placed text and strokes, raster resolution, external resources, and conservative page overflow. report.unchecked lists coverage limits for CSS, complex bounds, and other features that need visual inspection. A clean report is not a certification of arbitrary SVG. See the preflight guide.

Known limitations

A gradient-filled <mask> does not survive CairoSVG and vanishes from PDF and PNG exports without a warning.

Examples

The gallery holds journal figures from across the sciences, each one self-contained script. They need VecView and TeX; the transmon also needs VecWire. Run from the repository root:

python examples/transmon_figure.py      # superconducting qubits
python examples/graphene_bands.py       # condensed matter
python examples/microring_resonator.py  # photonics
python examples/optical_tweezers.py     # biophysics
python examples/hodgkin_huxley.py       # neuroscience
python examples/exoplanet_transit.py    # astrophysics
python examples/energy_landscape.py     # chemistry
python examples/climate_energy_balance.py # climate science
python examples/production_network.py    # economics
python examples/gallery.py              # all of them, with a preflight report

Two scripts show workflows rather than figures, with core dependencies only:

python examples/publication_preflight.py # a deliberately flawed figure and its report
python examples/thesis_plots.py --all    # a figure collection with a shared style

Each example writes its figures into examples/out/.

The suite

FigWorks is the composition layer of four projects developed together, each independently useful:

Project Produces Maturity
FigWorks composed, exported multi-panel figures alpha: core API still settling
VecTeX editable TeX equations as SVG fragments beta: on PyPI, API settled enough to build on
VecView layered 3D schematics as SVG documents alpha: on PyPI, API may change before 1.0
VecWire editable circuit schematics as SVG documents alpha: on PyPI, API may change before 1.0

All four emit vector SVG with stable ids and byte-identical output for identical input, so a figure can be regenerated from code, diffed in version control, and still hand-tuned in Inkscape. All four are pre-1.0 and make no backward-compatibility promise; deterministic output is a shared design contract within a fixed rendering environment. Pin the producer, TeX/converter, browser, and font versions when archiving a build. Manual SVG edits remain editable artifacts; they are not automatically merged into regenerated output.

The dependency edges are uneven by design: VecTeX is a runtime requirement, VecView and VecWire are optional, and none of the three depends on FigWorks.

Development

python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
ruff format --check .
ruff check .
ty check
pytest

Tests for VecView and VecWire skip when those packages are not installed, and tests that render VecTeX skip without pdflatex and dvisvgm. Rendering tests need TeX Gyre Heros, DejaVu Sans, and fontconfig's command-line tools (Debian/Ubuntu: fonts-texgyre fonts-dejavu-core fontconfig).

Documentation

The docs in docs/ are built with Zensical:

zensical serve    # preview at http://localhost:8000
zensical build    # static build into site/

License

MIT

Metadata

Release files for figworks 0.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for figworks 0.9.0
File Size Uploaded
figworks-0.9.0.tar.gz 91.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for figworks 0.9.0
File Interpreter ABI Platform
figworks-0.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 157.2 kB

Release files / figworks-0.9.0.tar.gz

Download URL figworks-0.9.0.tar.gz
Size 91.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0ca728f6238435c89fbd263002554156c7ea66da6e0eb976ef97bdb5269c038e
BLAKE2b-256 checksum
How to use checksums
f48572c375a2c6a07b0b18613feb5dad619c51e0f0bc2ac0de89515e62653ead
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / figworks-0.9.0-py3-none-any.whl

Download URL figworks-0.9.0-py3-none-any.whl
Size 65.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0c908ae21744c6aeac13ff050ac1dcd3bc0b551f1220bbb3ad1313edf9af7b12
BLAKE2b-256 checksum
How to use checksums
1cdeba2e3f1a3584090c82de3d139a7db822dca67e2eaa58665128d6cfece4c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.9.0 This release

2 release files

0.8.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page