Search code examples
pythonmatplotlibredrawblit

How can I redraw only certain matplotlib artists?


I'm working on a custom interactive figure for electrophysiology data, anywhere from 10-400 lines (EEG or MEG data channels) plotted as a LineCollection with offsets. It is often useful to have a vertical line to assess how signal features on different channels align temporally, so I have a button_press_event listener that creates an axvline (or updates the xdata of the line, if it already exists). Redrawing the axvline is expensive if there are lots of channels in the LineCollection, but the supposedly more efficient redraw method (ax.draw_artist(my_vline)) doesn't work at all (it is quite possible that I am simply misunderstanding how draw_artist is supposed to work).

Code for reproduction

import matplotlib.pyplot as plt
plt.ion()


def make_vline(event):
    ax = event.inaxes
    if getattr(ax, 'my_vline', None) is None:
        ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r')
    else:
        ax.my_vline.set_xdata(event.xdata)
    # I thought any 1 of these 3 lines would move the vline to the click location:
    ax.draw_artist(ax.my_vline)  # this has no visible effect
    ax.redraw_in_frame()  # TypeError (see below for traceback)
    ax.figure.canvas.draw_idle()  # works, but slow when figure has many lines


fig, ax = plt.subplots()
callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)

Actual outcome

  • If I use the ax.draw_artist(ax.my_vline) line, outcome is a blank axes no matter where I click (unless I then resize the figure, which triggers a redraw and then the line appears).

  • If i use the ax.redraw_in_frame() line, I get:

Traceback (most recent call last):
  File "/opt/miniconda3/envs/mnedev/lib/python3.8/site-packages/matplotlib/cbook/__init__.py", line 224, in process
    func(*args, **kwargs)
  File "<ipython-input-1-08572d18e6b3>", line 11, in make_vline
    ax.redraw_in_frame()
  File "/opt/miniconda3/envs/mnedev/lib/python3.8/site-packages/matplotlib/axes/_base.py", line 2778, in redraw_in_frame
    stack.push(artist.set_visible, artist.get_visible())
TypeError: push() takes 2 positional arguments but 3 were given
  • if I use ax.figure.canvas.draw_idle() it works as expected, but is really slow once the figure has actual data in it. Here is a longer code snippet you can run locally to see the slowness:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
plt.ion()
rng = np.random.default_rng()


def make_vline(event):
    ax = event.inaxes
    if getattr(ax, 'my_vline', None) is None:
        ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r', zorder=3)
    else:
        ax.my_vline.set_xdata(event.xdata)
    ax.figure.canvas.draw_idle()  # works, but slow when figure has many lines


def add_line_collection(ax):
    n_chans = 400
    n_times = 10001
    xs = np.linspace(0, 10, n_times)
    ys = rng.normal(size=(n_chans, n_times)) * 1e-6
    segments = [np.vstack((xs, ys[n])).T for n in range(n_chans)]
    yoffsets = np.arange(n_chans)
    offsets = np.vstack((np.zeros_like(yoffsets), yoffsets)).T
    lc = LineCollection(segments, offsets=offsets, linewidths=0.5, colors='k')
    ax.add_collection(lc)
    ax.set_xlim(xs[0], xs[-1])
    ax.set_ylim(yoffsets[0] - 0.5, yoffsets[-1] + 0.5)
    ax.set_yticks(yoffsets)


fig, ax = plt.subplots()
add_line_collection(ax)
callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)

Questions

  1. when would ax.draw_artist(my_artist) actually work / what is it supposed to do?
  2. Is my example a case where blitting would be beneficial?
  3. Any other ideas for how to speed up (re)drawing here?

Matplotlib version

  • Operating system: Xubuntu 20.04
  • Matplotlib version: 3.3.1 (conda-forge)
  • Matplotlib backend: Qt5Agg
  • Python version: 3.8.5
  • Jupyter version (if applicable): n/a
  • Other libraries: numpy 1.19.1 (conda-forge)

Solution

  • I solved this with blitting, based on the MPL blitting tutorial:

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.collections import LineCollection
    plt.ion()
    rng = np.random.default_rng()
    
    
    def make_vline(event):
        fig.canvas.restore_region(fig.my_bg)
        ax = event.inaxes
        if getattr(ax, 'my_vline', None) is None:
            ax.my_vline = ax.axvline(event.xdata, linewidth=4, color='r', zorder=3)
        else:
            ax.my_vline.set_xdata(event.xdata)
        ax.draw_artist(ax.my_vline)
        ax.figure.canvas.blit()
        ax.figure.canvas.flush_events()
    
    
    def add_line_collection(ax):
        n_chans = 400
        n_times = 10001
        xs = np.linspace(0, 10, n_times)
        ys = rng.normal(size=(n_chans, n_times)) * 1e-6
        segments = [np.vstack((xs, ys[n])).T for n in range(n_chans)]
        yoffsets = np.arange(n_chans)
        offsets = np.vstack((np.zeros_like(yoffsets), yoffsets)).T
        lc = LineCollection(segments, offsets=offsets, linewidths=0.5, colors='k')
        ax.add_collection(lc)
        ax.set_xlim(xs[0], xs[-1])
        ax.set_ylim(yoffsets[0] - 0.5, yoffsets[-1] + 0.5)
        ax.set_yticks(yoffsets)
    
    
    fig, ax = plt.subplots()
    add_line_collection(ax)
    callback_id = fig.canvas.mpl_connect('button_press_event', make_vline)
    plt.pause(0.1)
    fig.my_bg = fig.canvas.copy_from_bbox(fig.bbox)
    

    Note that this will not work if the figure is resized, you would need to rerun the copy_from_bbox line in a resize listener.