matplotlib animation FuncAnimation frames argument

7.9k views Asked by At

I'm using matplotlib animation by calling:

plot = animation.FuncAnimation(fig, update, frames=data_gen(a), init_func=init, interval=10, blit=True)

Here, "a" is an initial value for the data_gen function which looks like this:

data_gen(x)
    old_x = x
    while True:
        new_x = func(old_x)
        old_x = new_x
        yield new_x

The intention for this code is for data_gen to produce a new value for new_x each time the animated plot is updated.

BUT... this happens instead:

animation.py throws an error in the init() method of the FuncAnimation class.

The problem happens in this code:

elif iterable(frames):
    self._iter_gen = lambda: iter(frames)
    self.save_count = len(frames)

The error is "TypeError: object of type 'generator' has no len()"

It looks like data_gen is iterable but it has no len().

Here is more of the code for the init() method in the FuncAnimation class:

    # Set up a function that creates a new iterable when needed. If nothing
    # is passed in for frames, just use itertools.count, which will just
    # keep counting from 0. A callable passed in for frames is assumed to
    # be a generator. An iterable will be used as is, and anything else
    # will be treated as a number of frames.
    if frames is None:
        self._iter_gen = itertools.count
    elif isinstance(frames, collections.Callable):
        self._iter_gen = frames
    elif iterable(frames):
        self._iter_gen = lambda: iter(frames)
        self.save_count = len(frames)
    else:
        self._iter_gen = lambda: iter(list(range(frames)))
        self.save_count = frames

I'm not sure why my data_gen is not a collections.Callable. If it were, then len(frames) would never happen.

Any suggestions for what I should do about this would be appreciated!

1

There are 1 answers

4
tacaswell On BEST ANSWER

The solution is to either A) pre-generate all of your data and shove it into a list (if you really have a finite number of frames), ie data_list = list(data_gen) B) install from source off my branch that fixes this: PR #2634 or C) monkey patch matplotlib, that is just replace the buggy code in your installation with the fixed code at runtime

C) is the most fun ;)

# copied directly from the proposed fix
def monkey_patch_init(self, fig, func, frames=None, init_func=None, fargs=None,
             save_count=None, **kwargs):
    if fargs:
        self._args = fargs
    else:
        self._args = ()
    self._func = func

    # Amount of framedata to keep around for saving movies. This is only
    # used if we don't know how many frames there will be: in the case
    # of no generator or in the case of a callable.
    self.save_count = save_count

    # Set up a function that creates a new iterable when needed. If nothing
    # is passed in for frames, just use itertools.count, which will just
    # keep counting from 0. A callable passed in for frames is assumed to
    # be a generator. An iterable will be used as is, and anything else
    # will be treated as a number of frames.
    if frames is None:
        self._iter_gen = itertools.count
    elif six.callable(frames):
        self._iter_gen = frames
    elif iterable(frames):
        self._iter_gen = lambda: iter(frames)
        if hasattr(frames, '__len__'):
            self.save_count = len(frames)
    else:
        self._iter_gen = lambda: xrange(frames).__iter__()
        self.save_count = frames

    # If we're passed in and using the default, set it to 100.
    if self.save_count is None:
        self.save_count = 100

    self._init_func = init_func

    # Needs to be initialized so the draw functions work without checking
    self._save_seq = []

    TimedAnimation.__init__(self, fig, **kwargs)

    # Need to reset the saved seq, since right now it will contain data
    # for a single frame from init, which is not what we want.
    self._save_seq = []

We now have a function monkey_patch_init which is (what I claim) is the fixed code. We now just replace the buggy __init__ function with this function:

matplotlib.animation.FuncAnimation.__init__ = monkey_patch_init

and your animation should work.

ani = animation.FuncAnimation(fig, update, frames=data_gen(a), init_func=init, interval=10, blit=True)

As a side note, don't use plot as a variable name. A lot of people bulk import pyplot into their name space (such as by ipython --pylab) which has plot -> matplotlib.pyplot.plot -> plt.gca().plot hence it makes makes it more likely your code will confuse someone.