Matplotlib stops animating after first frame

2019-04-20 02:18发布

问题:

I'm trying to animate two subplots, each with multiple lines. I am using Matplotlib, and I am using the FuncAnimation, which is used by many of the animation examples.

Using animation:

If I try to animate it, I only get the result of the first frame:

Without using animation:

If I manually call my update_lines function, it works fine.

Code:

Below is the full code (uncommenting the 3 indicated lines in main() works, but I would like to see it update in real-time, hence trying to use the animation).

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


def make_subplots():
    def setup_axes(axes):
        for ax in axes:
            ax.set_xbound(0, 100)  # bound will change as needed.
            ax.set_ylim(0, 1)  # limit won't change automatically.

    def make_lines(axes):
        labels = ('a', 'b', 'c')
        lines = []
        for ax in axes:
            ax_lines = []
            for label in labels:
                x, y = [0], [0]
                line, = ax.plot(x, y, label=label)  # comma for unpacking.
                ax_lines.append((line, x, y))
            lines.append(ax_lines)
        return lines

    fig, axes = plt.subplots(2, 1, sharex=True, sharey=True)
    lines = make_lines(axes)
    setup_axes(axes)
    return fig, axes, lines


def make_data():
    for i in xrange(100):
        print 'make_data():', i
        data = dict()
        for label in ('a', 'b', 'c'):
            from random import random
            data[label] = random()
        yield (i + 1, data)


def update_lines(data, lines):
    print 'update_lines():', data, lines
    updated_lines = []
    for ax_lines in lines:
        for line, x, y in ax_lines:
            label = line.get_label()
            x.append(data[0])
            y.append(data[1][label])
            line.set_data(x, y)
            updated_lines.append(line)


def main():
    fig, axes, lines = make_subplots()

    # Uncomment these 3 lines, and it works!
    # new_data = make_data()
    # for data in new_data:
    #     update_lines(data, lines)

    FuncAnimation(fig=fig,
                  func=update_lines,
                  frames=make_data,
                  fargs=(lines,),
                  interval=10,
                  blit=False)

    plt.show()


if __name__ == '__main__':
    main()

回答1:

(Undocumented?) Hooks

So, I was digging around the source-code of matplotlib.animation.Animation, and I noticed these lines in the __init__() function:

# Clear the initial frame
self._init_draw()

# Instead of starting the event source now, we connect to the figure's
# draw_event, so that we only start once the figure has been drawn.
self._first_draw_id = fig.canvas.mpl_connect('draw_event', self._start)

Sounds familiar...

This looks right so far. The self._init_draw() call draws my first frame immediately. Then the animation-object hooks into the figure-object and waits for the figure to be shown before attempting to draw any more frames for the animation.

Eureka!

The keyword is: animation-object. Since I wasn't planning on using the animation instance later (for example, to draw a movie), I didn't assign it to a variable. In fact, I was being yelled at by pyflakes because Local variable '...' is assigned to but never used.

But because all of the functionality relies on the hook, when the canvas is finally shown I presume Python's garbage collection has removed the Animation instance---since it was never assigned to a variable---and therefore the animation can never be started.

The fix

Simply assign the instance FuncAnimation instance to a variable, and everything works as expected!

anim = FuncAnimation(fig=fig,
                     func=update_lines,
                     frames=make_data,
                     fargs=(lines,),
                     interval=10,
                     blit=False)