Animating a Phase-Shifting Sine Wave with Matplotlib FuncAnimation
Build a reusable sine-wave animation, preview it interactively, and export MP4 or GIF files with verified frame timing.
13 Jul 2026, 15:48 UTC

The problem: motion that a static plot cannot show
A single sine-wave image tells the viewer what the curve looks like at one instant. If the wave shifts horizontally over time—say, a phase change that represents a moving signal—a still image cannot convey that motion. You need an animation that plays the shift frame by frame, either live in a window or as a file you can drop into a slide deck.
The useful takeaway: Matplotlib's FuncAnimation ties a fixed figure to an update function. You define what changes each frame; Matplotlib handles the redraw loop and can pipe the frames to a video or GIF writer. You do not need to save individual PNGs and stitch them yourself.
What you need before writing code
This guide assumes Matplotlib 3.0 or later, where the animation API and writer selection are stable. You also need NumPy to generate the wave data. For file export, you need at least one writer:
- ffmpeg (system-level) for MP4 output.
- Pillow (Python package) for GIF output.
A backend is the library that draws the figure. Interactive previews require a GUI backend such as TkAgg or Qt5Agg. A writer is the encoder that turns rendered frames into a compressed file. Blitting is an optimization that redraws only the changed artists; it works well when the background is static.
Building the figure and update loop
Keep the axes fixed so the viewer's eye tracks the moving wave rather than a rescaling plot. Precompute the x-values once; only the y-values change per frame.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
fig, ax = plt.subplots()
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.5, 1.5)
ax.set_xlabel("x")
ax.set_ylabel("sin(x + shift)")
x = np.linspace(0, 2 * np.pi, 1000)
line, = ax.plot([], [], lw=2)
The init function draws the first, empty state. Returning the line object tells Matplotlib which artist to redraw when blitting is enabled.
def init():
line.set_data([], [])
return line,
The update function receives a frame number. Here, the phase shift is proportional to the frame index, producing a constant left-to-right drift.
def update(frame):
shift = 0.1 * frame
y = np.sin(x + shift)
line.set_data(x, y)
return line,
Instantiate the animation with a small frame count first. A short test run finishes quickly and exposes logic errors before you commit to a long render.
anim = animation.FuncAnimation(
fig,
update,
init_func=init,
frames=10, # start small; increase after the logic works
interval=100, # milliseconds between frames
blit=True
)
Previewing interactively versus exporting a file
Interactive preview and file export serve different purposes. Use plt.show() when you want to watch the animation in a GUI window and confirm the motion feels right. Use anim.save() when you need a portable artifact.
# Interactive preview (requires a GUI backend)
plt.show()
# File export (works in headless environments too)
anim.save("sine_wave.mp4", writer="ffmpeg", fps=30)
# or
anim.save("sine_wave.gif", writer="pillow", fps=30)
If you run the script on a remote server or inside a CI pipeline, there may be no display. In that case, skip plt.show() and go straight to saving. You can force a non-interactive backend with matplotlib.use("Agg") before importing pyplot.
Installing writers and running save commands
Install Pillow from your project's terminal or command prompt. In a virtual environment no elevated permissions are needed; in a system-wide Python installation you may need --user or administrator rights.
pip install pillow
ffmpeg is a system package, not a Python library. On Ubuntu or Debian, install it with sudo (requires admin password):
sudo apt-get install ffmpeg
On macOS with Homebrew, run brew install ffmpeg. On Windows, download the build and add the binary directory to your PATH. After installation, verify it from any terminal:
ffmpeg -version
Expected check: the command prints version information rather than "command not found". If Matplotlib cannot find ffmpeg, the save call raises an error and the console may warn that no writer is available.
Verifying the result
A practical way to check the output is to inspect the file's metadata. For MP4, run ffprobe sine_wave.mp4 in a terminal. Look for the frame rate and duration; duration should approximately equal frames / fps. For GIF, most image viewers report frame count and total time, or you can use mediainfo sine_wave.gif.
If you want a faster sanity check, temporarily set frames=10 and interval=100. The animation should finish in about one second, letting you confirm the update logic without waiting for a long render.
Limitations and troubleshooting
High frame counts and high DPI settings consume memory because each frame is rendered before being handed to the writer. If the process crashes, reduce the figure DPI or the frame count rather than increasing system swap.
Blitting improves performance but assumes the background does not change. If you later add a moving title, changing axis limits, or a second artist that appears and disappears, set blit=False. The animation will be slower, but you avoid flickering and missing elements.
In a Jupyter notebook, you may not have ffmpeg installed. You can embed the animation directly in the output cell without an external writer:
from IPython.display import HTML
HTML(anim.to_jshtml())
This renders the frames as JavaScript in the browser. It is useful for sharing a notebook, but it is not a video file and will not play in a standard media player.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.