I would like to have an iteratively plotted graph that allows for skipping to the next frame, stopping it and coming back to a previous frame.
I have looked at matplotlib Animation module which would be perfect if there was a way to implement the previous frame functionality (like run Animation backwards for a few frames when a key is pressed)
It would be nice to something like this:
def update_frame(i, data):
fig.set_data(data[i])
but in a way that I could explicitly manage whether the i iterator increases or decreases.
Is there a way to do that in matplotlib?
Should I look for a different python module?
The FuncAnimation
class allows to supply a generator function to the frames
argument. This function would be expected to yield a value that is supplied to the updating function for each step of the animantion.
The FuncAnimation
doc states:
frames
: iterable, int, generator function, or None, optional
[..]
If a generator function, then must have the signature
def gen_function() -> obj:
In all of these cases, the values in frames is simply passed through to the user-supplied func and thus can be of any type.
We can now create a generator function which yields integers either in forward or in backward direction such that the animation runs forwards or backwards . To steer the animation, we might use matplotlib.widgets.Button
s and also create a one-step forward or backward functionality. This is similar to my answer to the question about looping through a set of images.
The following is a class called Player
which subclasses FuncAnimation
and incoorporates all of this, allowing to start and stop the animation. It can be instantiated similarly to FuncAnimation
,
ani = Player(fig, update, mini=0, maxi=10)
where update
would be an updating function, expecting an integer as input, and mini
and maxi
denote the minimal and maximal number that the function could use. This class stores the value of the current index (self.i
), such that if the animation is stopped or reverted it will restart at the current frame.
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.func, 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.fig.canvas.draw_idle()
def setup(self, pos):
playerax = self.fig.add_axes([pos[0],pos[1], 0.22, 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)
self.button_oneback = matplotlib.widgets.Button(playerax, label=ur'$\u29CF$')
self.button_back = matplotlib.widgets.Button(bax, label=ur'$\u25C0$')
self.button_stop = matplotlib.widgets.Button(sax, label=ur'$\u25A0$')
self.button_forward = matplotlib.widgets.Button(fax, label=ur'$\u25B6$')
self.button_oneforward = matplotlib.widgets.Button(ofax, label=ur'$\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)
### 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()
For a proper working answer with the Animation module see the answer of ImportanceOfBeingErnest
I have multiple issues with your intended functionality. How would the animation's progress work together with reversing? Would there be a video, but pressing a button starts playing it back? Or should there be individual steps of frames? I'm not sure I understand how an animation could be coupled to this reversal feature; I picture matplotlib animations to be essentially movies.
My other issue is a technical one: I'm not sure this can be done with matplotlib animations. The docs explain that a FuncAnimation
superficially performs
for d in frames:
artists = func(d, *fargs)
fig.canvas.draw_idle()
plt.pause(interval)
where frames
is essentially an iterable. It doesn't seem straightforward to me to dynamically adjust frames
during the animation, so this is a technical obstacle.
Actually, the functionality you described works much better in my head in a widget-based approach. Buttons could propagate the "animation", or you could have a check button that modifies whether the next step goes forward or backward. Here's a simple proof of concept of what I mean:
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np # just for dummy data generation
# generate dummy data
ndat = 20
x = np.linspace(0,1,ndat)
phi = np.linspace(0,2*np.pi,100,endpoint=False)
dat = np.transpose([x[:,None]*np.cos(phi),x[:,None]*np.sin(phi)],(1,2,0))
# create figure and axes
fig = plt.figure()
ax_pl = plt.subplot2grid((5,5),(0,0),colspan=5,rowspan=3) # axes_plot
ax_bl = plt.subplot2grid((5,5),(4,0),colspan=2,rowspan=1) # axes_button_left
ax_br = plt.subplot2grid((5,5),(4,3),colspan=2,rowspan=1) # axes_button_right
# create forward/backward buttons
butt_l = Button(ax_bl, '\N{leftwards arrow}') # or u'' on python 2
butt_r = Button(ax_br, '\N{rightwards arrow}') # or u'' on python 2
# create initial plot
# store index of data and handle to plot as axes property because why not
ax_pl.idat = 0
hplot = ax_pl.scatter(*dat[ax_pl.idat].T)
ax_pl.hpl = hplot
ax_pl.axis('scaled')
ax_pl.axis([dat[...,0].min(),dat[...,0].max(),
dat[...,1].min(),dat[...,1].max()])
ax_pl.set_autoscale_on(False)
ax_pl.set_title('{}/{}'.format(ax_pl.idat,dat.shape[0]-1))
# define and hook callback for buttons
def replot_data(ax_pl,dat):
'''replot data after button push, assumes constant data shape'''
ax_pl.hpl.set_offsets(dat[ax_pl.idat])
ax_pl.set_title('{}/{}'.format(ax_pl.idat,dat.shape[0]-1))
ax_pl.get_figure().canvas.draw()
def left_onclicked(event,ax=ax_pl,dat=dat):
'''try to decrement data index, replot if success'''
if ax.idat > 0:
ax.idat -= 1
replot_data(ax,dat)
def right_onclicked(event,ax=ax_pl,dat=dat):
'''try to increment data index, replot if success'''
if ax.idat < dat.shape[0]-1:
ax.idat += 1
replot_data(ax,dat)
butt_l.on_clicked(left_onclicked)
butt_r.on_clicked(right_onclicked)
plt.show()
Note that I'm not really experienced with matplotlib widgets or GUIs in general, so don't expect the above to conform with best practices in the subject. I also added some additional parameters to be passed here and there, because I have an aversion to using global names, but this might be somewhat superstitious in this context; I honestly can't tell. Also, if you're defining these objects inside a class or a function, make sure to keep a reference to the widgets otherwise they might become unresponsive when accidentally garbage-collected.
The resulting figure has an axes for plotting the scatter plots, and there are two buttons to increment the slicing index. The data is shaped (ndat,100,2)
, where the trailing indices define 100 points in 2d space. A specific state:
(It doesn't have to be this ugly, I just didn't want to fiddle with the design.)
I could even imagine a setup where a timer automatically updates the plot, and the direction of the update can be set with a widget. I'm not sure how this could be done properly, but I would try to pursue this path for the kind of visualization you seem to be after.
Also note that the above approach is entirely missing blitting and other optimizations that FuncAnimation
would do, but this will hopefully not interfere with your visualization.