11

I can't figure out how to get an animated title working on a FuncAnimation plot (that uses blit). Based on http://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/ and Python/Matplotlib - Quickly Updating Text on Axes, I've built an animation, but the text parts just won't animate. Simplified example:

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

vls = np.linspace(0,2*2*np.pi,100)

fig=plt.figure()
img, = plt.plot(np.sin(vls))
ax = plt.axes()
ax.set_xlim([0,2*2*np.pi])
#ttl = ax.set_title('',animated=True)
ttl = ax.text(.5, 1.005, '', transform = ax.transAxes)

def init():
    ttl.set_text('')
    img.set_data([0],[0])
    return img, ttl
def func(n):
    ttl.set_text(str(n))
    img.set_data(vls,np.sin(vls+.02*n*2*np.pi))
    return img, ttl

ani = animation.FuncAnimation(fig,func,init_func=init,frames=50,interval=30,blit=True)

plt.show()

If blit=True is removed, the text shows up, but it slows way down. It seems to fail with plt.title, ax.set_title, and ax.text.

Edit: I found out why the second example in the first link worked; the text was inside the img part. If you make the above 1.005 a .99, you'll see what I mean. There probably is a way to do this with a bounding box, somehow...

Community
  • 1
  • 1
Henry Schreiner
  • 905
  • 1
  • 7
  • 18

3 Answers3

20

See Animating matplotlib axes/ticks and python matplotlib blit to axes or sides of the figure?

So, the problem is that in the guts of animation where the blit backgrounds are actually saved (line 792 of animation.py), it grabs what is in the axes bounding box. This makes sense when you have multiple axes being independently animated. In your case you only have one axes to worry about and we want to animate stuff outside of the axes bounding box. With a bit of monkey patching, a level of tolerance for reaching into the guts of mpl and poking around a bit, and acceptance of the quickest and dirtyest solution we can solve your problem as such:

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

def _blit_draw(self, artists, bg_cache):
    # Handles blitted drawing, which renders only the artists given instead
    # of the entire figure.
    updated_ax = []
    for a in artists:
        # If we haven't cached the background for this axes object, do
        # so now. This might not always be reliable, but it's an attempt
        # to automate the process.
        if a.axes not in bg_cache:
            # bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.bbox)
            # change here
            bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.figure.bbox)
        a.axes.draw_artist(a)
        updated_ax.append(a.axes)

    # After rendering all the needed artists, blit each axes individually.
    for ax in set(updated_ax):
        # and here
        # ax.figure.canvas.blit(ax.bbox)
        ax.figure.canvas.blit(ax.figure.bbox)

# MONKEY PATCH!!
matplotlib.animation.Animation._blit_draw = _blit_draw

vls = np.linspace(0,2*2*np.pi,100)

fig=plt.figure()
img, = plt.plot(np.sin(vls))
ax = plt.axes()
ax.set_xlim([0,2*2*np.pi])
#ttl = ax.set_title('',animated=True)
ttl = ax.text(.5, 1.05, '', transform = ax.transAxes, va='center')

def init():
    ttl.set_text('')
    img.set_data([0],[0])
    return img, ttl

def func(n):
    ttl.set_text(str(n))
    img.set_data(vls,np.sin(vls+.02*n*2*np.pi))
    return img, ttl

ani = animation.FuncAnimation(fig,func,init_func=init,frames=50,interval=30,blit=True)

plt.show()

Note that this may not work as expected if you have more than one axes in your figure. A much better solution is to expand the axes.bbox just enough to capture your title + axis tick labels. I suspect there is code someplace in mpl to do that, but I don't know where it is off the top of my head.

Community
  • 1
  • 1
tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • This works at full speed! Wish it was included in matplotlib, vs. monkey patched, but works well! – Henry Schreiner Jul 10 '13 at 21:51
  • @HenrySchreiner Your edit should have been accepted. (I just re-did it). If this solved your problem can you accept the answer (the big gray checkbox on the left). – tacaswell Jul 10 '13 at 22:17
  • Ideally, the text bounding box should be used (since it is passed from the animation function), but for some reason it is not being used (even though I think it is in artists). This is a reasonable hack to fix it for now. :) Thanks! – Henry Schreiner Jul 11 '13 at 01:44
  • @HenrySchreiner The animation code is still a little rough around the edges. There are good reasons for not doing it this way the main code (see my caveats). If you want to improve this in main line, please do. They devs on mpl are very friendly. – tacaswell Jul 11 '13 at 14:49
  • If I use `repeat=False` in the `FuncAnimation` call, and then wait until the animation finishes and zoom in somewhere, the drawing disappears. – Stanley Bak Sep 22 '16 at 16:09
  • I had problems with this workaround, when I tried to return artist objects to blit from multiple axes. In that case some things were not correctly erased. – mxmlnkn Feb 06 '17 at 02:36
  • How would you set the axis text if you are using an iterator function with funcanimation? From what I can tell in the docs the iterator returns a tuple of line objects which just plot to the axis. – bretcj7 Sep 06 '17 at 03:50
9

To add to tcaswell's "monkey patching" solution, here is how you can add animation to the axis tick labels. Specifically, to animate the x-axis, set ax.xaxis.set_animated(True) and return ax.xaxis from the animation functions.

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

def _blit_draw(self, artists, bg_cache):
    # Handles blitted drawing, which renders only the artists given instead
    # of the entire figure.
    updated_ax = []
    for a in artists:
        # If we haven't cached the background for this axes object, do
        # so now. This might not always be reliable, but it's an attempt
        # to automate the process.
        if a.axes not in bg_cache:
            # bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.bbox)
            # change here
            bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.figure.bbox)
        a.axes.draw_artist(a)
        updated_ax.append(a.axes)

    # After rendering all the needed artists, blit each axes individually.
    for ax in set(updated_ax):
        # and here
        # ax.figure.canvas.blit(ax.bbox)
        ax.figure.canvas.blit(ax.figure.bbox)

# MONKEY PATCH!!
matplotlib.animation.Animation._blit_draw = _blit_draw

vls = np.linspace(0,2*2*np.pi,100)

fig=plt.figure()
img, = plt.plot(np.sin(vls))
ax = plt.axes()
ax.set_xlim([0,2*2*np.pi])
#ttl = ax.set_title('',animated=True)
ttl = ax.text(.5, 1.05, '', transform = ax.transAxes, va='center')

ax.xaxis.set_animated(True)

def init():
    ttl.set_text('')
    img.set_data([0],[0])
    return img, ttl, ax.xaxis

def func(n):
    ttl.set_text(str(n))
    vls = np.linspace(0.2*n,0.2*n+2*2*np.pi,100)
    img.set_data(vls,np.sin(vls))
    ax.set_xlim(vls[0],vls[-1])
    return img, ttl, ax.xaxis

ani = animation.FuncAnimation(fig,func,init_func=init,frames=60,interval=200,blit=True)

plt.show()
eric
  • 642
  • 1
  • 9
  • 11
1

You must call

plt.draw()

After

ttl.set_text(str(n))

Here there is a very simple example of a text-animation inside a figure "without FuncAnimation()". Try it, you will see if it is useful for you.

import matplotlib.pyplot as plt
import numpy as np
titles = np.arange(100)
plt.ion()
fig = plt.figure()
for text in titles:
    plt.clf()
    fig.text(0.5,0.5,str(text))
    plt.draw()
Antony Hatchkins
  • 31,947
  • 10
  • 111
  • 111
Pablo
  • 2,443
  • 1
  • 20
  • 32
  • 2
    That does draw it, but it slows it down the same amount `blit=False` does. I was hoping to just redraw the text. – Henry Schreiner Jul 10 '13 at 01:25
  • Why you don't simply avoid animation and makes your own animation with plt.ion()? With it you have a lot of control, and you are sure what are you doing every frame... Take a look at http://stackoverflow.com/questions/17444655/dynamically-updating-a-graphed-line-in-python/17480397#17480397 – Pablo Jul 10 '13 at 01:27
  • fig=plt.figure() img, = plt.plot(np.sin(vls)) ax = plt.axes() ax.set_xlim([0,2*2*np.pi]) title = ax.text(.5, 1.005, '', transform = ax.transAxes) plt.ion() for i in range(100): img.set_data(vls,np.sin(vls+.02*(i%50)*2*np.pi)) title.set_text(str(i%50)) plt.draw() – Henry Schreiner Jul 10 '13 at 01:44
  • I tested that, it's still slow, but I might be able to manually call `canvas.blit`. I thought that animation was a newer/preferred method (over building it manually). – Henry Schreiner Jul 10 '13 at 01:55
  • Ok... I'm not an expert with animation method. Your problem tell me that it's not working very good... or we missed something. I will investigate the performance in the next days... It's a very interesting problem! – Pablo Jul 10 '13 at 02:00
  • The second example on the first link had something similar to what I'm doing but it worked. It might be because my titles are outside the redrawn 'img' part?... – Henry Schreiner Jul 10 '13 at 02:36
  • Pablo, what you are describing is _exactly_ what the animation module does behind the scenes, except it wraps it all up in a (nominally) easy to use package and gives you things like one line saving to a mp4 and setting up timers to get a given frame rate. The issue here is getting blitting (which redraws only a sub-set of the artists on top of a fixed base layer) to work, which is _much_ faster than re-drawing all of the elements in the figure. – tacaswell Jul 10 '13 at 04:05
  • If you read the question, you will see that my answer is a valid answer. – Pablo Jul 10 '13 at 16:18
  • 3
    The question is 'how do I make blit work on artists outside of an axes' – tacaswell Jul 10 '13 at 22:18
  • Setting blit=False causes it to work with the original problem code, just slowly, like ion(). But thanks for your help! – Henry Schreiner Jul 11 '13 at 01:45