While looking for a way to make animated interactive plot using matplotlib, I encountered this piece of code on Stack overflow documentation:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.widgets import Slider
TWOPI = 2*np.pi
fig, ax = plt.subplots()
t = np.arange(0.0, TWOPI, 0.001)
initial_amp = .5
s = initial_amp*np.sin(t)
l, = plt.plot(t, s, lw=2)
ax = plt.axis([0,TWOPI,-1,1])
axamp = plt.axes([0.25, .03, 0.50, 0.02])
# Slider
samp = Slider(axamp, 'Amp', 0, 1, valinit=initial_amp)
def update(val):
# amp is the current value of the slider
amp = samp.val
# update curve
l.set_ydata(amp*np.sin(t))
# redraw canvas while idle
fig.canvas.draw_idle()
# call update function on slider value change
samp.on_changed(update)
plt.show()
This code does almost exactly what I'm looking for, but I would wish to animate the plot, i.e. make the slider moves automatically from left to right, for instance progressing of 0.01 every second. Is there any simple way of doing that? Knowing that I also want to keep the manual control on the slider (using click event).
But did you know that it is also possible to create interactive plots with matplotlib directly, provided you are using an interactive backend? This article will look at two such backends and how they render interactivity within the notebooks, using only matplotlib.
Animations in Matplotlib can be made by using the Animation class in two ways: By calling a function over and over: It uses a predefined function which when ran again and again creates an animation. By using fixed objects: Some animated artistic objects when combined with others yield an animation scene.
Matplotlib can be used in an interactive or non-interactive modes. In the interactive mode, the graph display gets updated after each statement. In the non-interactive mode, the graph does not get displayed until explicitly asked to do so.
You may adapt the code from this answer to include a slider.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import mpl_toolkits.axes_grid1
import matplotlib.widgets
class Player(FuncAnimation):
def __init__(self, fig, func, frames=None, init_func=None, fargs=None,
save_count=None, mini=0, maxi=100, pos=(0.125, 0.92), **kwargs):
self.i = 0
self.min=mini
self.max=maxi
self.runs = True
self.forwards = True
self.fig = fig
self.func = func
self.setup(pos)
FuncAnimation.__init__(self,self.fig, self.update, frames=self.play(),
init_func=init_func, fargs=fargs,
save_count=save_count, **kwargs )
def play(self):
while self.runs:
self.i = self.i+self.forwards-(not self.forwards)
if self.i > self.min and self.i < self.max:
yield self.i
else:
self.stop()
yield self.i
def start(self):
self.runs=True
self.event_source.start()
def stop(self, event=None):
self.runs = False
self.event_source.stop()
def forward(self, event=None):
self.forwards = True
self.start()
def backward(self, event=None):
self.forwards = False
self.start()
def oneforward(self, event=None):
self.forwards = True
self.onestep()
def onebackward(self, event=None):
self.forwards = False
self.onestep()
def onestep(self):
if self.i > self.min and self.i < self.max:
self.i = self.i+self.forwards-(not self.forwards)
elif self.i == self.min and self.forwards:
self.i+=1
elif self.i == self.max and not self.forwards:
self.i-=1
self.func(self.i)
self.slider.set_val(self.i)
self.fig.canvas.draw_idle()
def setup(self, pos):
playerax = self.fig.add_axes([pos[0],pos[1], 0.64, 0.04])
divider = mpl_toolkits.axes_grid1.make_axes_locatable(playerax)
bax = divider.append_axes("right", size="80%", pad=0.05)
sax = divider.append_axes("right", size="80%", pad=0.05)
fax = divider.append_axes("right", size="80%", pad=0.05)
ofax = divider.append_axes("right", size="100%", pad=0.05)
sliderax = divider.append_axes("right", size="500%", pad=0.07)
self.button_oneback = matplotlib.widgets.Button(playerax, label='$\u29CF$')
self.button_back = matplotlib.widgets.Button(bax, label='$\u25C0$')
self.button_stop = matplotlib.widgets.Button(sax, label='$\u25A0$')
self.button_forward = matplotlib.widgets.Button(fax, label='$\u25B6$')
self.button_oneforward = matplotlib.widgets.Button(ofax, label='$\u29D0$')
self.button_oneback.on_clicked(self.onebackward)
self.button_back.on_clicked(self.backward)
self.button_stop.on_clicked(self.stop)
self.button_forward.on_clicked(self.forward)
self.button_oneforward.on_clicked(self.oneforward)
self.slider = matplotlib.widgets.Slider(sliderax, '',
self.min, self.max, valinit=self.i)
self.slider.on_changed(self.set_pos)
def set_pos(self,i):
self.i = int(self.slider.val)
self.func(self.i)
def update(self,i):
self.slider.set_val(i)
### using this class is as easy as using FuncAnimation:
fig, ax = plt.subplots()
x = np.linspace(0,6*np.pi, num=100)
y = np.sin(x)
ax.plot(x,y)
point, = ax.plot([],[], marker="o", color="crimson", ms=15)
def update(i):
point.set_data(x[i],y[i])
ani = Player(fig, update, maxi=len(y)-1)
plt.show()
Here is a simple adaptation of your code to add animation:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.widgets import Slider
TWOPI = 2*np.pi
fig, ax = plt.subplots()
t = np.arange(0.0, TWOPI, 0.001)
initial_amp = .5
s = initial_amp*np.sin(t)
l, = plt.plot(t, s, lw=2)
ax = plt.axis([0,TWOPI,-1,1])
axamp = plt.axes([0.25, .03, 0.50, 0.02])
# Slider
samp = Slider(axamp, 'Amp', 0, 1, valinit=initial_amp)
# Animation controls
is_manual = False # True if user has taken control of the animation
interval = 100 # ms, time between animation frames
loop_len = 5.0 # seconds per loop
scale = interval / 1000 / loop_len
def update_slider(val):
global is_manual
is_manual=True
update(val)
def update(val):
# update curve
l.set_ydata(val*np.sin(t))
# redraw canvas while idle
fig.canvas.draw_idle()
def update_plot(num):
global is_manual
if is_manual:
return l, # don't change
val = (samp.val + scale) % samp.valmax
samp.set_val(val)
is_manual = False # the above line called update_slider, so we need to reset this
return l,
def on_click(event):
# Check where the click happened
(xm,ym),(xM,yM) = samp.label.clipbox.get_points()
if xm < event.x < xM and ym < event.y < yM:
# Event happened within the slider, ignore since it is handled in update_slider
return
else:
# user clicked somewhere else on canvas = unpause
global is_manual
is_manual=False
# call update function on slider value change
samp.on_changed(update_slider)
fig.canvas.mpl_connect('button_press_event', on_click)
ani = animation.FuncAnimation(fig, update_plot, interval=interval)
plt.show()
The main change is the addition of the update_plot
function, which is used to make a FuncAnimation
in the second to last line. The animation increments from the last slider value that was set.
The variable is_manual
keeps track of when the user has clicked on the slider. After the user clicks on it, the variable is set to True
and the animation will no longer update the plot.
To resume animation, I added an on_click
function which sets is_manual = False
when the user clicks somewhere on the canvas OTHER than the slider.
Since this is a quick-and-dirty script I left variables as global, but you could easily write it up in a proper class.
Note that calling samp.set_val
implicitly calls the update_slider
function, which is also called when the user clicks directly on the slider, so we have to reset is_manual
in the update_plot
function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With