Search code examples
pythonmatplotlibmatplotlib-animation

Managing dynamic plotting in matplotlib Animation module


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?


Solution

  • 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 enter image description here or backwards enter image description here. To steer the animation, we might use matplotlib.widgets.Buttons and also create a one-step forward enter image description here or backward enter image description here 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=u'$\u25C0$')
            self.button_stop = matplotlib.widgets.Button(sax, label=u'$\u25A0$')
            self.button_forward = matplotlib.widgets.Button(fax, label=u'$\u25B6$')
            self.button_oneforward = matplotlib.widgets.Button(ofax, label=u'$\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()
    

    enter image description here


    Note: This hasn't been written in a way to allow for blitting.