In Jupyter Lab on Windows, the matplotlib animation API documentation breaks when a no-op edit is added in the update loop.
%matplotlib ipympl
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
xdata, ydata = [], []
ln, = ax.plot([], [], 'ro')
def init():
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1, 1)
return ln,
def update(frame):
xdata.append(frame)
ydata.append(np.sin(frame))
#if False:
# xdata = xdata[1:]
# ydata = ydata[1:]
ln.set_data(xdata, ydata)
return ln,
ani = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128),
init_func=init, blit=True)
plt.show()
The above is verbatim the code provided in the documentation, plus three comments in update
. With the 3 lines commented, the code works; a sine wave is displayed from start to finish over small intervals.
However, when the three lines are uncommented, the animation never starts. My goal was to show a fixed-size sliding window of the function at a time. I started with the condition if len(xdata) > 10:
. I learned that the condition never has to be met for the animation to break. In fact, the condition can be impossible.
The conditional statement
if len(xdata) > 10:
pass
does NOT cause the animation to fail, indicating that it's the block within the condition - not the condition itself - that is the breaking change.
I have also tried setting blit=False
and removing the return statements, but the same behavior happens - the animation never starts.
Versions:
ipykernel==6.29.4
ipympl==0.9.6
ipython==8.25.0
ipywidgets==8.1.5
jupyterlab==4.3.5
matplotlib==3.10.0
numpy==1.26.4
The problem is how Python scoping works and the if False:
attempt was further a red herring misleading you away from the real issue assignment vs. in-place modification of your variables.
Consider back to the conditional if len(xdata) > 10:
, when you do xdata = xdata[1:]
in update()
, you're not modifying the global xdata
list. Instead, you're creating a new local variable called xdata that only exists in the scope of the local update()
function. The original global xdata remains unchanged. So nothing was happening because you call the update(frame)
function and then when the condition triggers it creates the local variables that 'shadow' the globals. When the function encounters the return
, those local variables are discarded and then the next time it is still working with the unmodified global xdata
and ydata
.
You could fix your original code where you wanted to use len(xdata) > 10:
by adding global
, like so:
%matplotlib ipympl
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
xdata, ydata = [], []
# Change to a line plot instead of just points
ln, = ax.plot([], [], 'r-') # Use 'r-' for a red line
# Maximum number of points to display
max_points = 25
def init():
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1, 1)
return ln,
def update(frame):
global xdata, ydata
xdata.append(frame)
ydata.append(np.sin(frame))
# Only keep the most recent max_points
if len(xdata) > max_points:
xdata.pop(0) # Remove the oldest point
ydata.pop(0)
ln.set_data(xdata, ydata)
return ln,
ani = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128),
init_func=init, blit=True, interval=50)
plt.show()
Optionally you could use list methods that modify in place and don't assign. Like xdata.pop(0)
.
So what was going on when you tried if False:
? I'm not quite sure but you weren't getting closer to what you wanted anyway and so I feel you were making an XY problem there.
Alternative to do what you stated as your goal to making a sliding window would be the following, I think:
%matplotlib ipympl
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# Create figure and axis
fig, ax = plt.subplots(figsize=(10, 6))
# Set up the data
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x) # Using sine function for demonstration
# Initialize an empty line
line, = ax.plot([], [], 'b-', lw=2)
# Set axis limits
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1, 1)
ax.set_xlabel('x')
ax.set_ylabel('sin(x)')
ax.set_title('Animated Sine Function (25 points at a time)')
ax.grid(True)
# Window size (number of points to show at once)
window_size = 25
def init():
"""Initialize the animation"""
line.set_data([], [])
return line,
def animate(i):
"""Animation function - i is the frame number"""
# Calculate start and end indices for the current window
start_idx = i % (len(x) - window_size + 1)
end_idx = start_idx + window_size
# Update the line data
line.set_data(x[start_idx:end_idx], y[start_idx:end_idx])
return line,
# Create animation
ani = FuncAnimation(fig, animate, frames=len(x)-window_size+1,
init_func=init, blit=True, interval=100)
plt.tight_layout()
plt.show()
This handles the starting over/looping a little better than your approach.