Python Figures
Generate figures with matplotlib, seaborn, or plotly in Python and embed them seamlessly in your Typst documents.
How It Works
NovaType bridges Python and Typst through a simple decorator-based workflow:
- Write Python functions that create matplotlib figures
- Decorate them with
@nova.figure("name") - Reference them in Typst with
pyplot("name") - Run
nova compile— figures are generated and embedded automatically
Generated figures are cached as SVGs. When your data or code changes, only affected figures are regenerated.
Setup
Install the Python package
$ pip install nova-typst
Project structure
my-project/
├── main.typ # Typst document
├── nova.toml # Project configuration
├── figures/
│ ├── __init__.py # Required (can be empty)
│ ├── plots.py # Your figure functions
│ └── analysis.py # More figures
└── data/
└── results.csv # Data files
Minimal configuration
# nova.toml
[python]
python = "python"
figures_dir = "figures"
Writing Figures
The @nova.figure decorator
import nova
import matplotlib.pyplot as plt
import numpy as np
@nova.figure("sine-wave")
def plot_sine():
x = np.linspace(0, 2 * np.pi, 100)
plt.plot(x, np.sin(x), linewidth=2)
plt.xlabel("x")
plt.ylabel("sin(x)")
Do not call plt.savefig() or plt.show() — NovaType handles saving automatically. Just create your plot and let the decorator do the rest.
Decorator parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
string | required | Unique identifier, used as pyplot("name") in Typst |
depends |
list | None |
File paths this figure depends on (triggers regeneration on change) |
format |
string | "svg" |
Output format: "svg" (recommended) or "png" |
dpi |
int | 150 |
Resolution for PNG output (ignored for SVG) |
Data-driven figures with dependencies
Use depends to track external data files. When any dependency changes, the figure is automatically regenerated:
import nova
import matplotlib.pyplot as plt
import pandas as pd
@nova.figure("revenue-chart", depends=["data/revenue.csv"])
def plot_revenue():
df = pd.read_csv("data/revenue.csv")
plt.bar(df["year"], df["revenue"])
plt.ylabel("Revenue (M EUR)")
Multiple figures per file
You can define as many figures as you want in a single Python file:
import nova
import matplotlib.pyplot as plt
@nova.figure("bar-chart")
def plot_bars():
plt.bar(["A", "B", "C"], [10, 25, 15])
@nova.figure("pie-chart")
def plot_pie():
plt.pie([40, 30, 20, 10], labels=["A", "B", "C", "D"])
Embedding in Typst
Import and use pyplot
#import ".nova/pyplot.typ": pyplot
// As a numbered figure with caption
#figure(
pyplot("sine-wave", width: 80%),
caption: [A sine wave generated with matplotlib]
) <fig:sine>
// Reference it
As shown in @fig:sine, the function is periodic.
// Inline usage (without figure wrapper)
Here is a preview: #pyplot("sine-wave", width: 30%)
pyplot parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
string | required | Figure name (must match the @nova.figure decorator) |
width |
length | auto |
Image width (e.g., 80%, 12cm) |
height |
length | auto |
Image height |
fit |
string | "contain" |
Fit mode: "cover", "contain", "stretch" |
alt |
string | auto |
Alt text for accessibility |
Configuration
Full [python] section in nova.toml:
[python]
python = "python" # Python executable
figures_dir = "figures" # Directory with figure scripts
cache_dir = ".nova/cache" # Where generated SVGs are stored
timeout = 60 # Timeout per figure (seconds)
venv = ".venv" # Virtual environment path (optional)
| Option | Type | Default | Description |
|---|---|---|---|
python |
string | "python" |
Python executable name or path |
figures_dir |
string | "figures" |
Directory containing Python figure scripts |
cache_dir |
string | ".nova/cache" |
Directory for cached SVGs |
timeout |
int | 60 |
Maximum execution time per figure (seconds) |
venv |
string | — | Path to virtual environment (auto-detects the Python executable inside) |
Caching
Figures are cached in .nova/cache/ to avoid unnecessary regeneration. A figure is only re-executed when:
- Its source file has changed (detected via SHA-256 hash)
- Any file listed in
dependshas been modified - The cached SVG is missing
To force regeneration, delete the cache directory:
$ rm -rf .nova/cache
$ nova compile main.typ
Add .nova/ to your .gitignore. The cache is a build artifact that will be regenerated automatically.
CLI Options
# Normal compilation (generates figures then compiles)
$ nova compile main.typ
# Skip Python figure generation (use cached figures only)
$ nova compile main.typ --no-python
# Watch mode (regenerates figures on each change)
$ nova watch main.typ
Complete Example
Python side
# figures/plots.py
import nova
import matplotlib.pyplot as plt
import numpy as np
@nova.figure("training-loss", depends=["data/training.csv"])
def plot_training():
data = np.loadtxt("data/training.csv", delimiter=",", skiprows=1)
epochs, loss, val_loss = data.T
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(epochs, loss, label="Training")
ax.plot(epochs, val_loss, label="Validation", linestyle="--")
ax.set_xlabel("Epoch")
ax.set_ylabel("Loss")
ax.legend()
@nova.figure("confusion-matrix")
def plot_confusion():
matrix = np.array([[85, 10, 5],
[8, 82, 10],
[3, 12, 85]])
fig, ax = plt.subplots()
ax.imshow(matrix, cmap="Blues")
ax.set_xlabel("Predicted")
ax.set_ylabel("Actual")
Typst side
#import ".nova/pyplot.typ": pyplot
= Results
== Training Curves
#figure(
pyplot("training-loss", width: 90%),
caption: [Training and validation loss over epochs]
) <fig:loss>
As shown in @fig:loss, the model converges after ~50 epochs.
== Classification Performance
#figure(
pyplot("confusion-matrix", width: 60%),
caption: [Confusion matrix on the test set]
) <fig:confusion>