Speeding up Matplotlib plotting times for real-time monitoring purposes

Working on a side project I had to plot a parameter read from a nano-second-range sensor, and naturally I got curious how fast I can push Matplotlib. Needless to say, Matplotlib is meant to be used for publication quality graphs, and has been never meant to be used for performance plotting.

Matplotlib refresh rate with and without Blit

My initial test shows that with background "caching" I can make it ~1.5 times faster. Not very much gain. I will try next PyQtGraph1 which looks the most often updated codebase between high-performance plotting libs, and based on initial tests can run up to 400 Hz on my machine (i7 iMac), giving a 2.5 ms time resolution.

import time

# for Mac OSX
import matplotlib
matplotlib.use('TkAgg')

import matplotlib.pylab as plt
import random

from mpltools import style
style.use('ggplot')

fig = plt.figure()
ax1 = fig.add_subplot(1, 1, 1)

def test_fps(use_blit=True):

    ax1.cla()
    ax1.set_title('Sensor Input vs. Time -' + 'Blit [{0:3s}]'.format("On" if use_blit else "Off"))
    ax1.set_xlabel('Time (s)')
    ax1.set_ylabel('Sensor Input (mV)')

    plt.ion()  # Set interactive mode ON, so matplotlib will not be blocking the window
    plt.show(False)  # Set to false so that the code doesn't stop here

    cur_time = time.time()
    ax1.hold(True)

    x, y = [], []
    times = [time.time() - cur_time]  # Create blank array to hold time values
    y.append(0)

    line1, = ax1.plot(times, y, '.-', alpha=0.8, color="gray", markerfacecolor="red")

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

    if use_blit:
        background = fig.canvas.copy_from_bbox(ax1.bbox) # cache the background

    tic = time.time()

    niter = 200
    i = 0
    while i < niter:

        fields = random.random() * 100

        times.append(time.time() - cur_time)
        y.append(fields)

        # this removes the tail of the data so you can run for long hours. You can cache this
        # and store it in a pickle variable in parallel.

        if len(times) > 50:
           del y[0]
           del times[0]

        xmin, xmax, ymin, ymax = [min(times) / 1.05, max(times) * 1.1, -5,110]

        # feed the new data to the plot and set the axis limits again
        line1.set_xdata(times)
        line1.set_ydata(y)

        plt.axis([xmin, xmax, ymin, ymax])

        if use_blit:
            fig.canvas.restore_region(background)    # restore background
            ax1.draw_artist(line1)                   # redraw just the points
            fig.canvas.blit(ax1.bbox)                # fill in the axes rectangle
        else:
            fig.canvas.draw()

        i += 1

    fps = niter / (time.time() - tic)
    print "Blit [{0:3s}] -- FPS: {1:.1f}, time resolution: {2:.4f}s".format("On" if use_blit else "Off", fps, 1/fps)
    return fps

fps1 = test_fps(use_blit=True)
fps2 = test_fps(use_blit=False)

print "-"*50
print "With Blit ON plotting is {0:.2f} times faster.".format(fps1/fps2)

Edit 1: Following the advice on bastibe.de, one can also use

ax1.draw_artist(ax1.patch)
ax1.draw_artist(line1)
fig.canvas.update()
fig.canvas.flush_events()

instead of

fig.canvas.restore_region(background)    # restore background
ax1.draw_artist(line1)                   # redraw just the points
fig.canvas.blit(ax1.bbox)                # fill in the axes rectangle
fig.canvas.flush_events()

Note that this time I am using Qt4Agg, which would be ideal since most of my development is using Qt4, but has one tiny problem of blocking the diagram until done. Hence one can use plt.pause(0.001) or simply fig.canvas.flush_events() without the lazy sleep() in the pause function. This change increase the speed to 1.85 times the original. Also apparently it won't break the plot if you resize it.

Edit 2: One of the reasons that this is rather slow still is due to the fact that I am rescaling the axis each step -- redrawing axis is costly. If you set the axis limits to fixed number the speedup becomes amazing, a whooping 600 Hz and 1 ms resolution; A 14 times speedup.