Skip to main content

Turn any static matplotlib script into an animated GIF or MP4 by sweeping one or more variables

Project description

mpl-animator

Turn any static matplotlib script into an animated GIF or MP4 by sweeping a variable - no rewriting required.

Features

  • Zero boilerplate - point it at an existing script, it figures out the rest
  • Multi-variable animation - sweep multiple variables simultaneously with --var azim spin elev --range "0,360" "0,6.28" "20,40"; all variables share the same frame clock, each advances independently
  • Explicit value lists - animate over any discrete set of values with --values "1,5,10,50,100" instead of a linear range; frame count is inferred automatically
  • AST-based dependency tracking - automatically identifies which variables and calculations need to update each frame, across all animated variables
  • Parallel rendering - renders frames across all CPU cores via multiprocessing, falls back to sequential automatically
  • GIF and MP4 output - export as animated GIF (Pillow) or MP4 (ffmpeg); just add --format mp4
  • Loop control - --loop 0 (forever, default), --loop 1 (play once), --loop N
  • Ping-pong - --ping-pong plays forward then reversed for seamless looping GIFs
  • Reverse - --reverse sweeps each range end → start
  • Axis rotation - animate azim with ax.view_init to rotate 3D plots; animate angle with ax.set_theta_offset to spin polar plots
  • Math expressions in ranges - --range "0,2*pi" just works
  • 2D, 3D, and polar plots - handles plot_surface, scatter3D, subplots, polar axes, and more
  • Seaborn & pandas support - recognizes sns.heatmap, sns.lineplot, df.plot(), pd.plotting.scatter_matrix(), and 30+ other third-party draw calls out of the box
  • Augmented assignment safety - scripts using y += ..., x -= ..., etc. inside animated sections are automatically handled with proper global declarations (no UnboundLocalError)
  • Graceful frame errors - if a frame crashes (e.g. animated value hits an invalid domain), that frame is skipped with a warning instead of aborting the entire render
  • Cross-platform & portable - works on Windows, macOS, and Linux; uses repr() for file paths, UTF-8 encoding throughout, and os.makedirs for output directories
  • Single-file, standalone - mpl_animator.py can be dropped into any project with no install required; only depends on matplotlib, numpy, and Pillow (no exotic dependencies)
  • Library API - importable as a Python module for use in notebooks or pipelines
  • Extensively tested - validated against all 510 official matplotlib gallery examples; 484 automated tests total

Install

pip install mpl-animator

Or just copy the file - no install needed:

# copy mpl_animator.py into your project, then use it directly
python mpl_animator.py my_plot.py --var t --range "0,1"

Usage

If installed via pip, use the mpl-animator command. If using the file directly, replace mpl-animator with python mpl_animator.py - everything else is identical.

# Basic: animate variable `f` from 3 to 60 (outputs GIF by default)
mpl-animator wave_static.py --var f --range "3,60"

# Export as MP4 instead of GIF (requires ffmpeg)
mpl-animator plot.py --var t --range "0,2*pi" --format mp4

# Math expressions in range, custom frame count and FPS
mpl-animator plot.py --var t --range "0,2*pi" --frames 60 --fps 30

# Rotate a 3D plot by animating the camera azimuth
mpl-animator my_3d_plot.py --var azim --range "0,360" --frames 72 --fps 20

# Multi-variable: camera orbits (azim) while the object spins (spin) and camera rises (elev)
mpl-animator orbit_static.py --var azim spin elev --range "0,360" "0,6.28" "20,40" --frames 90 --fps 25 --ping-pong

# Ping-pong loop (plays forward then reversed — great for seamless GIFs)
mpl-animator plot.py --var t --range "0,1" --frames 60 --ping-pong

# Control output quality and parallelism
mpl-animator plot.py --var alpha --range "0,1" --dpi 150 --workers 8

# Custom output filename
mpl-animator plot.py --var t --range "0,1" --out my_animation.gif

# Explicit values: animate over specific discrete values (frame count = len(values))
mpl-animator plot.py --var n --values "5,10,50,200,1000"

# Multi-variable explicit values: one comma-separated list per variable
mpl-animator plot.py --var a b --values "1,2,5,10" "0.1,0.5,1.0,2.0"

This generates a <script>_animated.py file. Run it to produce the output:

python wave_static_animated.py              # parallel (default)
python wave_static_animated.py --sequential # single-threaded fallback

Examples

Example 1 - Wave & spectrum (2D)

wave_static.py plots a signal and its frequency spectrum for a fixed frequency f:

# wave_static.py  (key lines)
f   = 10.0                          # <- variable to animate
t   = np.linspace(0, 1, 1000)
y   = np.sin(2*np.pi*f*t) + 0.4*np.sin(2*np.pi*2*f*t)

freqs    = np.fft.rfftfreq(len(t), d=t[1]-t[0])
spectrum = np.abs(np.fft.rfft(y))

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 6))
ax1.plot(t, y, 'royalblue', lw=1.5)
ax2.plot(freqs, spectrum, 'tomato', lw=1.5)
plt.show()

Animate f from 3 Hz to 60 Hz:

python mpl_animator.py examples/wave_static.py --var f --range "3,60" --frames 60 --fps 20
python wave_static_animated.py

The animator detects that y, spectrum depend on f, moves them into the per-frame update(), and keeps the figure/axes creation static - so only the data redraws each frame.

wave animation


Example 2 - 3D Lissajous curve

lissajous_3d_static.py draws a 3D Lissajous figure for fixed frequency ratio a:

# lissajous_3d_static.py  (key lines)
a  = 3.0          # <- variable to animate
b  = 2.0
c  = 1.0

t = np.linspace(0, 2 * np.pi, 1000)
x = np.sin(a * t + delta)
y = np.sin(b * t)
z = np.sin(c * t)

fig = plt.figure(figsize=(8, 6))
ax  = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z, c=colors, s=2, alpha=0.8)
ax.set_title(f"3D Lissajous  a={a:.1f}, b={b:.1f}, c={c:.1f}")
plt.show()

Animate a from 1 to 6, sweeping through different curve topologies:

python mpl_animator.py examples/lissajous_3d_static.py --var a --range "1,6" --frames 80 --fps 20
python lissajous_3d_static_animated.py

For 3D plots the animator calls fig.clear() and recreates the axes each frame (required to preserve the projection='3d' state), then re-runs all drawing commands with the new value of a.

lissajous animation



Example 3 - Cinematic 3D orbit (multi-variable)

orbit_static.py draws a torus knot for fixed camera position and object rotation:

# orbit_static.py  (key lines)
azim = 45.0          # camera azimuth in degrees      <- animated
spin = 0.0           # object self-rotation (radians) <- animated
elev = 20.0          # camera elevation in degrees    <- animated

# Object geometry: rotate the knot around its own Z axis
cx_rot = cx * np.cos(spin) - cy * np.sin(spin)
cy_rot = cx * np.sin(spin) + cy * np.cos(spin)

fig = plt.figure(figsize=(7, 7))
ax  = fig.add_subplot(111, projection='3d')
ax.plot_surface(xs, ys, zs, cmap='plasma', alpha=0.92)
ax.view_init(elev=elev, azim=azim)   # <- camera controlled by both azim and elev
plt.show()

All three variables sweep simultaneously from the same frame clock — the camera orbits a full 360°, the object spins one full turn, and the camera gently cranes upward. --ping-pong plays the sequence forward then reversed for a seamless loop:

python mpl_animator.py examples/orbit_static.py \
    --var azim spin elev \
    --range "0,360" "0,6.28318" "20,40" \
    --frames 90 --fps 25 --dpi 120 --ping-pong
python orbit_static_animated.py --sequential

The animator identifies that cx_rot, cy_rot, and the tube surface xs/ys/zs all depend on spin, and that ax.view_init depends on azim and elev — all three dependency chains are tracked and placed in update() automatically.

orbit animation


How it works

  1. Parses your script's AST to find which variables depend on the animated one
  2. Splits code into static (run once), dynamic (recalculated per frame), and plot (redrawn per frame)
  3. Generates a new script with sequential (PNG+stitch) and parallel (multiprocessing) renderers — each frame is a standalone PNG, stitched into GIF or MP4 at the end

Library usage

from mpl_animator import animate

src = open("my_plot.py").read()

# Single variable
animated_code = animate(src, var="t", range_str="0,6.28", frames=60, fps=25)
open("my_plot_animated.py", "w").write(animated_code)

# Multiple variables — pass lists of equal length
animated_code = animate(
    src,
    var=["azim", "spin", "elev"],
    range_str=["0,360", "0,6.28", "20,40"],
    frames=90, fps=25, ping_pong=True,
)

# Export as MP4
animated_code = animate(src, var="t", range_str="0,6.28", fmt="mp4")

# Explicit values: animate over a specific set of values (frame count is inferred)
animated_code = animate(src, var="n", values="5,10,50,200,1000")
animated_code = animate(src, var="n", values=[5, 10, 50, 200, 1000])

# Multi-variable explicit values
animated_code = animate(src, var=["a", "b"], values=["1,2,5,10", "0.1,0.5,1.0,2.0"])

# Ping-pong loop (seamless forward+reverse)
animated_code = animate(src, var="t", range_str="0,1", ping_pong=True, loop=0)

Supported plot types

2D (plot, scatter, bar, hist, contour, imshow, ...), 3D (plot_surface, scatter3D, ...), polar, and anything else matplotlib draws.

Tests

The animator has been extensively tested against all 510 official matplotlib gallery examples. Of those, 271 are directly animatable (the rest are interactive widgets, event-driven demos, or scripts with no scalar variable to sweep) — and the animator produces valid, runnable Python for every single one.

pytest tests/test_animator.py -v          # unit + integration tests (208 tests)
pytest tests/test_gallery.py -v           # all matplotlib gallery examples (276 tests)
pytest tests/ -v                          # everything (484 tests)
pytest tests/ -v -m slow                  # slow tests that generate actual GIFs/MP4s

Test coverage:

  • 208 unit and integration tests covering AST parsing, dependency tracking, code generation, validation, multi-variable animation, explicit value lists, augmented assignment scoping, seaborn/pandas draw recognition, per-frame error handling, 53 edge-case fixtures, and 10 real-world scripts
  • 276 parametrized tests — one per animatable official matplotlib gallery example, spanning every plot category: line, bar, scatter, histogram, contour, heatmap, 3D surface, 3D wireframe, polar, pie, errorbar, fill_between, quiver, stem, and more

Known limitations

  • Stateful / accumulating scripts - scripts that build up results iteratively (e.g. appending to a list across a loop) re-run from scratch each frame, which is correct for pure functional plots but may produce unexpected results for accumulating ones
  • if __name__ == "__main__": guards - code inside a __name__ guard is treated as a single block; if the animated variable is assigned inside the guard, the animator may not partition it correctly
  • Multiprocessing via exec() - when running the generated code via exec() rather than as a standalone file, the parallel renderer can't pickle the worker function and falls back to sequential mode automatically
  • Variable domain safety - the animator has no awareness of valid domains for the animated variable; sweeping into invalid ranges (e.g. negative values for a count parameter) may cause per-frame errors (which are now skipped gracefully)

Author: Basem Rajjoub

Built with the assistance of Claude Code

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mpl_animator-0.1.7.tar.gz (27.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mpl_animator-0.1.7-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file mpl_animator-0.1.7.tar.gz.

File metadata

  • Download URL: mpl_animator-0.1.7.tar.gz
  • Upload date:
  • Size: 27.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for mpl_animator-0.1.7.tar.gz
Algorithm Hash digest
SHA256 c1eaf2496ca77d51675ad5e7b2dbee955433e62816652a87139e0f29c633a513
MD5 832dbd0721578e737c507879e6fa6228
BLAKE2b-256 0f75ac6b065d9ac73530e2cd32d7ec1188d7328833d34e86cf56a2564919a64b

See more details on using hashes here.

File details

Details for the file mpl_animator-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: mpl_animator-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for mpl_animator-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 90a8db9520098317956eaabec0fe4fbae329587e2dbaa74014003dea71d26214
MD5 88a63ab7589183b00622d8042266a18b
BLAKE2b-256 1fe962d7837be781b7b84d688142b40529b85ace262b2ba71574a37e252e893c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page