2

I'm trying to use blitting with pylab to animate my plots at a fast frame rate; the code below seems to work fine, but plots new data on top of the old rather than replotting, so that I end up with a figure filling up with lines rather than one animated line (in each subplot). Any suggestions to get a single animated line (in each subfigure) at as fast a frame rate as possible greatly appreciated.

import pylab
import time
import threading
import random
import math

time_series, cos_series, sin_series = [], [], []
MAX = 100

# This generates new data for the plot

def data_generator():

    while True:
        time.sleep(0.1)
        ts = time.time()
        time_series.append(ts)
        cos_series.append(math.sin( ts ))
        sin_series.append(math.cos( ts ))

        if len(cos_series) > MAX:
            cos_series.pop(0)

        if len(sin_series) > MAX:
            sin_series.pop(0)

        if len(time_series) > MAX:
            time_series.pop(0)

if __name__ == '__main__':

    # Run the receiving function in a separate thread.

    thread = threading.Thread(target=data_generator)
    thread.daemon = True
    thread.start()

    fig = pylab.figure()

    ax = fig.add_subplot(211)
    bx = fig.add_subplot(212)

    ax.grid(True)
    bx.grid(True)

    print(len(time_series),len(sin_series),len(cos_series))

    fig.show()
    fig.canvas.draw()

    line1, = ax.plot(time_series, sin_series, '-.k', animated=True)
    line2, = bx.plot(time_series, cos_series, 'r+-', animated=True)

    ax.legend(['Sin(x)'])
    bx.legend(['Cos(x)'])

    ax.set_ylim([-1,1])
    bx.set_ylim([-1,1])

    background_a = [fig.canvas.copy_from_bbox(ax.bbox)]
    background_b = [fig.canvas.copy_from_bbox(bx.bbox)]

    timer = 0
    t_start = time.time()

    # Continuously update plot

    while True:

        time.sleep(0.1)

        line1.set_data(time_series,sin_series)
        ax.set_xlim([time_series[0],time_series[-1]])
        line2.set_data(time_series,cos_series)
        bx.set_xlim([time_series[0],time_series[-1]])

        ax.draw_artist(line1)
        bx.draw_artist(line2)

        fig.canvas.restore_region(background_a)
        fig.canvas.restore_region(background_b)

        fig.canvas.blit(ax.bbox)
        fig.canvas.blit(bx.bbox)

        timer += 1
        print('FPS = ',timer/(time.time() - t_start))
4
  • any reason you aren't using matplotlib.animation? Commented Sep 18, 2013 at 22:28
  • and is there any way you can reduce the amount of code here? Commented Sep 18, 2013 at 22:29
  • no good reason for not using matplotlib.animation - is this faster, or just cleaner code? Commented Sep 18, 2013 at 23:04
  • all the blitting and timer details are taken care of already and there is a clean way to save to mp4. Just throw a function and some data at it and you are good to go Commented Sep 18, 2013 at 23:05

1 Answer 1

2

There are two problems with your code.

Firstly, when you do this:

background_a = [fig.canvas.copy_from_bbox(ax.bbox)]
background_b = [fig.canvas.copy_from_bbox(bx.bbox)]

you shouldn't put your buffer objects in a list - restore_region just takes the buffer objects directly, so you should just do this instead:

background_a = fig.canvas.copy_from_bbox(ax.bbox)
background_b = fig.canvas.copy_from_bbox(bx.bbox)

Secondly, in your rendering loop you need to restore the background before you draw any of your updated line artists on top, otherwise you'll always be drawing the background on top of your moving lines. Move those lines above your draw_artist calls, like this:

fig.canvas.restore_region(background_a)
fig.canvas.restore_region(background_b)

ax.draw_artist(line1)
bx.draw_artist(line2)

fig.canvas.blit(ax.bbox)
fig.canvas.blit(bx.bbox)

Now everything should work fine.

Update

If you want the x-axis to also be updated during the animation, things get a little bit more complicated. Firstly you'll need to set the x-axis to be animated for both sets of axes:

ax = fig.add_subplot(211)
bx = fig.add_subplot(212)
ax.xaxis.set_animated(True)
bx.xaxis.set_animated(True)

The axis bounding box (ax.bbox) doesn't contain the tick labels, so in order to get a large enough region to restore during the rendering loop you'll need to cache a larger region of the figure canvas, e.g. the whole figure bounding box:

figbackground = fig.canvas.copy_from_bbox(fig.bbox)

And to restore the background:

fig.canvas.restore_region(figbackground)

At each timepoint you need to force the x-axis to be re-drawn as well as the lines:

ax.draw_artist(line1)
bx.draw_artist(line2)
ax.xaxis.draw(fig.canvas.renderer)
bx.xaxis.draw(fig.canvas.renderer)

And finally, when you do the blitting you need to use the axes clipboxes, which contain the tick labels, rather than the bounding boxes, which do not:

fig.canvas.blit(ax.clipbox)
fig.canvas.blit(bx.clipbox)

With these changes the tick labels and the x-gridlines will get updated, but so far I haven't figured out how exactly to get the y-gridlines and the legend to be drawn correctly. Hopefully this gives you some idea of how to go about doing this.

Also tcaswell is right to suggest looking at the Animation class - for your case it might work out to be a lot simpler, although I think it's also good to have an understand of how blitting works under the hood.

Sign up to request clarification or add additional context in comments.

1 Comment

this works great, thanks, but the axis tick labels do not update, i.e. they are fixed at their initial values - any idea how to update these dynamically?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.