Managing dynamic plotting in matplotlib Animation module
Asked Answered
N

2

6

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?

Nevels answered 8/7, 2017 at 11:54 Comment(0)
A
21

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.

Amylolysis answered 8/7, 2017 at 17:42 Comment(5)
Thank you for explaining that. It somehow did not occur to me that this could be solved by using a generator.Nevels
I've tried feeding a generator to FuncAnimation's frames, and the generator is streaming data into my animator fine up until the last iteration where I get a StopIteration Traceback inside .../site-packages/matplotlib/animation.py", line 1750, in _init_draw self._draw_frame(next(self.new_frame_seq()))Goutweed
@importanceofbeingernest I've also tried setting repeat=False and interval=1Goutweed
Thank you ! This should be a matplotlib exampleBrochure
I'm sorry if I haven't posted this comment in the correct location in this thread. I've tried experimenting but I don't see how to modify this so that when the figure opens, the plot isn't already running. (I'm sure it can be done by modifying one line of code.)Biostatics
P
1

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:

example result

(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.

Progestin answered 8/7, 2017 at 15:15 Comment(5)
You said "It doesn't seem straightforward to me to dynamically adjust frames during the animation". You may look at my answer to this question on how that could be done. The main point is that frames can be given a generator function.Amylolysis
@Amylolysis thanks, I also did say that I'm a GUI noob;) I did see the generator function option, but it made me seem that there's no simple way to manipulate the function between callbacks. Good job!Capitulation
Thanks, this answer was really helpfulNevels
Apologies! I'll move my suggestion to a more appropriate location in the discussion. Thanks.Biostatics
@Biostatics no worries. But this is an answer from 2017, and probably the only time I looked at mpl animations :) If I can help you, do let me know. But Ernest's answer is probably better for your purposes.Capitulation

© 2022 - 2024 — McMap. All rights reserved.