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
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
ax.draw_artist(my_artist)
actually work / what is it supposed to do?Matplotlib version
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.