Preface
Goal: Animating Math Equation using matplotlib. Observe how each quadratic coefficient impact the shape.
The world is running so fast. The work requirement is also grow up. We need to catch-up with current visualization technique. It is a crime if I just stand in front of my notebook doing my job. I need to enhance my skill, from just static chart to dynamic plot.
We can visualize how each quadratic coefficient, impact the shape, impact the relative position the axes. In fact I just realize that my understanding of this parabolic, was wrong all these time. I could never righten my understanding without proper visualization.
There is other advantage as well. Instead of just sending PNG image to telegram? Why not sending mp4 video to telegram as well?
2D Plot Animation
The difficulties escalate quickly. Let’s animate previous simple plot.
Source Code
The python script is longer now. You can obtain the code in this following link:
Coefficient
We have only three coeffcients: a, b, c.
We are going to use three scene:
- Change a only: controls the curvature (parabola’s width and direction).
- Change b only: affects the tilt (linear shift).
- Change c only: shifts the entire curve up/down.
Scene | Changing Coefficient | Effect on Graph |
---|---|---|
0 | a (quadratic term) | Changes the curvature (parabola becomes wider or narrower). |
1 | b (linear term) | Tilts the parabola left/right (affects the vertex position). |
2 | c (constant term) | Moves the entire parabola up/down without changing its shape. |
The vertex is the turning point of the parabola.
Changing b
does not move the parabola purely left or right.
Instead, it moves diagonally.
Class Skeleton
class QuadraticAnimator:
def __init__(self):
...
def quadratic_function(self, x, a, b, c):
...
def update(self, frame):
...
def animate(self):
...
def save_animation(self,
...
Now we can run the animation script
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
class QuadraticAnimator:
...
# Run animation
if __name__ == "__main__":
animator = QuadraticAnimator()
animator.save_animation()
animator.animate()
Quadratic Function
What quadratic function that we use? Let’s compute y values for the quadratic value using this function.
def quadratic_function(self, x, a, b, c):
return a * x**2 + b * x + c
Initialization
The initialization has tdifferent parts.
- Common chart setting. Nothing special.
- Equation Text
- Initial Plot Values
- Animation Settings
Common chart setting.
Let’s start with common chart setting.
class QuadraticAnimator:
def __init__(self):
self.fig, self.ax = plt.subplots()
self.x = np.linspace(-10, 20, 400)
# Set up the plot
self.ax.set_xlim(-10, 20)
self.ax.set_ylim(-150, 100)
self.ax.axhline(y=0, color='k')
self.ax.axvline(x=0, color='k')
self.ax.grid()
self.ax.set_xlabel("X-Axis")
self.ax.set_ylabel("Y-Axis")
Equation Text
I separate the equation text, and the dynamic coefficient text.
self.eq_text = self.ax.text(
0.05, 0.9, r"$y = ax^2 + bx + c$",
transform=self.ax.transAxes, fontsize=16, color='b')
We need to pay attention to the dynamic coefficient. Because we are going to update it periodically.
self.coeff_text = self.ax.text(
0.05, 0.8, "", transform=self.ax.transAxes,
fontsize=14, color='r')
Initial Plot Values
Both line and points.
self.line, = self.ax.plot([], [], 'k')
self.points, = self.ax.plot([], [], 'bo')
Animation Settings
At last, the frame settings. The total frames is 240.
# Frames per scene
self.scene_duration = 60
# Frames to pause before next scene
self.pause_duration = 20
# Full cycle
self.total_frames = 3 * (
self.scene_duration + self.pause_duration)
Three is the number of scene.
Frame Breakdown Per Scene
We are going to separate the frames are split into three distinct regions! Each scene consists of:
- 60 frames of animation (changes happen)
- 20 frames of pause (nothing changes)
We are going to use modulo and division to compute this situation.
Frame Range | Calculation | Scene | Animation or Pause? |
---|---|---|---|
0 β 59 | 0 // 80 | 0 | Animate (Changing a) |
60 β 79 | 79 // 80 | 0 | Pause |
80 β 139 | 80 // 80 | 1 | Animate (Changing b) |
140 β 159 | 159 // 80 | 1 | Pause |
160 β 219 | 160 // 80 | 2 | Animate (Changing c) |
220 β 239 | 239 // 80 | 2 | Pause |
Run The Animation
The animate()
function creates and runs an animation,
using matplotlib.animation.FuncAnimation
.
def animate(self):
ani = animation.FuncAnimation(
self.fig, self.update,
frames=self.total_frames,
interval=100, blit=False, repeat=True)
plt.show()
FuncAnimation
works internally like this:
FuncAnimation
callsupdate(frame)
for “each frame”.- The
frame
number starts from 0 and goes up toself.total_frames - 1
. - Our
update(frame)
function changes the plotted data based onframe
. - After reaching the last frame, it starts over
(repeat=True)
.
Updating Animation.
To animate, we would rely on update function.
The structure of the update function follows,
common animation techniques used in Matplotlib’s FuncAnimation
.
Using this logic:
- Breaking the frames into scenes,
- Smoothly changing parameters, and
- Using trigonometric functions for oscillation.
The specific implementation is based on the general techniques, that are widely used concepts in animation and visualization.
Update Function
Update function for animation.
The update function is long and containing multiple steps.
- Scene Management (Frames and Scenes)
- Handle Pauses Between Scenes
- Normalize Time for Smooth Oscillation
- Adjust Quadratic Coefficients (Based on Scene)
- Compute New Function Values
- Update Text Display
- Return Updated Elements
Let’s break down step by step:
Scene Management (Frames and Scenes)
We need to determines which scene is active, and where we are within that scene.
scene = frame // (
self.scene_duration + self.pause_duration)
scene_frame = frame % (
self.scene_duration + self.pause_duration)
-
The total animation consists of multiple scenes (each modifying one coefficient).
-
A scene lasts
self.scene_duration
frames, followed by aself.pause_duration
. -
scene
tells us which coefficient is changing. -
scene_frame
tells us how far along we are in that scene.
Handle Pauses Between Scenes
We need to keeps the plot unchanged during pauses. Pause before switching to the next scene.
if scene_frame >= self.scene_duration:
return self.line, self.points, self.coeff_text
-
If
scene_frame
is within the pause period, nothing changes. -
The function immediately returns, skipping the update.
Normalize Time for Smooth Oscillation
We need to create smooth oscillation of coefficients over time. Normalize time within the scene, moves from +1 to -1 to +1 smoothly.
t = scene_frame / self.scene_duration
direction = np.cos(t * np.pi * 2)
-
t represents how far along we are in the scene (0 β 1).
-
np.cos(t * np.pi * 2) generates a smooth oscillation from +1 to -1 and back.
Adjust Quadratic Coefficients (Based on Scene)
We need to change one coefficient at a time, while keeping others constant.
if scene == 0: # Change a only
a = 2 * direction
b, c = - 12, -64
elif scene == 1: # Change b only
a, c = 1, -64
b = - 12 * direction
else: # Change c only
a, b = 1, -12
c = -64 * direction
- scene == 0: a oscillates between +2 and -2.
- scene == 1: b oscillates between +12 and -12.
- scene == 2: c oscillates between +64 and -64.
For simplicity you can rewrite as:
a, b, c = 1, -12, -64
if scene == 0:
a = 2 * direction
elif scene == 1:
b = -12 * direction
else:
c = -64 * direction
Compute New Function Values
Now we need to updates the quadratic curve and sample points.
y = self.quadratic_function(self.x, a, b, c)
self.line.set_data(self.x, y)
xdot = np.arange(-6, 16, 1)
ydot = self.quadratic_function(xdot, a, b, c)
self.points.set_data(xdot, ydot)
-
self.quadratic_function(self.x, a, b, c)
calculates newy
values. -
self.line.set_data(self.x, y)
updates the main curve. -
xdot
andydot
are sample points displayed as dots.
Update Text Display
We need to track by displaying the current values of a
, b
, and c
.
# Update dynamic text
self.coeff_text.set_text(
r"$a = {:.1f}, b = {:.1f}, c = {:.1f}$"\
.format(a, b, c))
- Formats the coefficient values to 1 decimal place.
- Updates the equation text on the plot
Return Updated Elements
Now we should ensure FuncAnimation
redraws the updated elements.
return self.line, self.points, self.coeff_text
- Returns the updated line, sample points, and text, so they appear in the animation.
Save Animation
As a bonus, we can save the animation result into video in mp4
format.
def save_animation(self,
filename="quadratic-animation-mpl.mp4", fps=30):
# Save the animation as a video.
ani = animation.FuncAnimation(
self.fig, self.update, frames=self.total_frames,
interval=100, blit=False, repeat=True)
writer = animation.FFMpegWriter(fps=fps)
ani.save(filename, writer=writer)
print(f"Animation saved as {filename}")
Video Result
This will show you this animation below:
Using Scatter
There is other alternative as well.
Original Plot
You can spot this line:
self.line, = self.ax.plot([], [], 'k')
self.points, = self.ax.plot([], [], 'bo')
And updating it as:
xdot = np.arange(-6, 16, 1)
ydot = self.quadratic_function(xdot, a, b, c)
self.points.set_data(xdot, ydot)
Using Scatter
Actually we can setup scatter for the dots.
You can spot this line:
self.line, = self.ax.plot([], [], 'k')
self.points = self.ax.scatter([], [])
And updating it as:
xdot = np.arange(-6, 16, 1)
ydot = self.quadratic_function(xdot, a, b, c)
self.points.set_offsets(np.c_[xdot, ydot])
Seaborn Styling
Seaborn is great for static plots but not really designed for animations. It builds on Matplotlib but adds extra layers that make updating elements tricky. For animations, I’d better stick to pure Matplotlib.
However we can utilize the seaborn style in matplotlib. The saqme logic, but cleaner look.
Source Code
You can obtain the code in this following link:
What’s changed?
Seaborn Styling
- Applied darkgrid style for a cleaner look.
- Used Seaborn color palette for lines and points.
- Improved text contrast for better readability.
The result looks like this below:
3D Surface Animation
We can apply the same method to the surface.
Source Code
You can obtain the code in this following link:
Coefficient
Now we have only six coeffcients: a, b, c, d, e, f.
We are still using meshgrid
to plot the surface.
Adding background music
I put background music form pixabay.com,
a jazz style piano made by WELC0MEΠ0
.
And combine using ffmpeg
command.
ffmpeg -i "/home/epsi/quadratic-surface.mp4" \
-i "/home/epsi/171220-jazz-style-piano-20557.mp3" \
-c:v copy -c:a aac -shortest \
"/home/epsi/quadratic-surface-with-music.mp4"
Video Result
The result will show you this animation below:
Looks nice right?
3D View Animation
Instead of the surface, we can aimate the camera only. So you can see the shape from different perspective.
Source Code
You can obtain the code in this following link:
With large number of frame, the viseo result will be big.
Adding background music
I put background music form pixabay.com,
named dancing rhythms driving elctro swing composition made by OpenMusicList
. And reduce the size using ffmpeg
command.
ffmpeg \
-i "/home/epsi/mpl/quadratic-surface-view.mp4" \
-i "/home/epsi/dancing-rhythms-driving-electro-swing-composition-149594.mp3" \
-c:v libx264 -crf 28 -r 15 -c:a aac \
-b:a 128k -b:v 500k \
-vf "scale=640:-2,format=yuv420p" \
-shortest -movflags +faststart \
"/home/epsi/quadratic-surface-view-with-music.mp4"
Video Result
The result will show you this animation below:
What is Next π€?
I will continue with manim
if I have time.
Conclusion
Thank you for visiting. We shall meet again. Farewell.