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:

  1. Write Python functions that create matplotlib figures
  2. Decorate them with @nova.figure("name")
  3. Reference them in Typst with pyplot("name")
  4. 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)")
Important

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:

To force regeneration, delete the cache directory:

$ rm -rf .nova/cache
$ nova compile main.typ
Tip

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>