Export as vector based graphics (SVG, EPS, PDF) throws an error for Evoked.plot()
Description of the problem
When using Evoked.plot() (assigning it to a figure handle) and then trying to export from that figure handle to a vector graphic (SVG, EPS or PDF), I get error messages like the one below (one for each axis / plot in the figure). Exporting as PNG works fine. Apparently, there seems to be a missing attribute copy_from_bbox.
Traceback (most recent call last):
File "/opt/Software4EEG/MNE/lib/python3.12/site-packages/matplotlib/cbook.py", line 361, in process
func(*args, **kwargs)
File "/opt/Software4EEG/MNE/lib/python3.12/site-packages/matplotlib/widgets.py", line 2167, in update_background
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'
Steps to reproduce
Use the code and the data from https://mne.tools/stable/auto_examples/visualization/channel_epochs_image.html (up to where epochs is created). Afterwards, use:
fig = epochs.average().plot()
fig.savefig('Trial.svg')
The error thrown after calling the last command.
fig.savefig('Trial.png') works without any problem, i.e., it is not a problem with generating the plot itself.
Link to data
See above. But I assume, it would produce the error whenever one assigns Evoked.plot() to a figure handle and then tries to save a vector graphics (that requires a bounding box).
Expected results
Export of the figure without any error messages.
Additional information: It appears as if in some cases the exported figures can be read by Inkscape, in other cases they seem to contain mistakes and Inkscape crashes. I could not determine yet, why the exported figures sometimes seems to have valid format.
Actual results
Export of the figure with an error message.
Resulting SVG-files work sometimes, but not always.
Additional information
Platform Linux-6.11.0-21-generic-x86_64-with-glibc2.39
Python 3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0]
Executable /opt/Software4EEG/MNE/bin/python
CPU Intel(R) Core(TM) Ultra 5 135U (14 cores)
Memory 30.8 GiB
Core
├☑ mne 1.9.0 (latest release)
├☑ numpy 2.2.4 (OpenBLAS 0.3.28 with 14 threads)
├☑ scipy 1.15.2
└☑ matplotlib 3.10.1 (backend=qtagg)
Numerical (optional)
├☑ sklearn 1.6.1
├☑ h5io 0.2.4
├☑ h5py 3.13.0
└☐ unavailable numba, nibabel, nilearn, dipy, openmeeg, cupy, pandas
Visualization (optional)
├☑ qtpy 2.4.3 (PySide6=6.9.0)
├☑ pyqtgraph 0.13.7
├☑ mne-qt-browser 0.7.1
└☐ unavailable pyvista, pyvistaqt, vtk, ipympl, ipywidgets, trame_client, trame_server, trame_vtk, trame_vuetify
Ecosystem (optional)
├☑ mne-bids 0.16.0
├☑ mne-icalabel 0.7.0
├☑ edfio 0.4.8
├☑ pybv 0.7.6
└☐ unavailable mne-nirs, mne-features, mne-connectivity, mne-bids-pipeline, neo, eeglabio, mffpy
FigureCanvasSVG is not a class defined by MNE so it's likely a matplotlib bug or that MNE uses a specific matplotlib artist that does not export to SVG. I would suggest to make a minimal code reproducer if you can. I would literally copy the mne-python plotting code in a notebook and see what line in the function I need to remove to make the SVG export pass. Then trim down as much code as possible without replicating the bug and then depending on the outcome report the bug in matplotlib or find a way out on the mne side.
my 2c
Message ID: @.***>
Hey, I'd like to work on this issue. Can you assign me this?
@AasmaGupta you are welcome to work on it. Before I assign you though, can you do as @agramfort suggested to isolate whether the bug is actually in MNE-Python or in matplotlib? If it's a matplotlib problem there's nothing further to be done in this repo.
@drammock I tested this as suggested:
Plain Matplotlib plotting and saving (SVG/PDF) works fine. MNE plotting with evoked.plot() fails on fig.savefig() with:
AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox' AttributeError: 'FigureCanvasPdf' object has no attribute 'copy_from_bbox'
This confirms the issue is specific to MNE’s plotting/export logic and not Matplotlib itself. I think you can assign me this then.
Plain Matplotlib plotting and saving (SVG/PDF) works fine.
When you say this, did you just create some plain matplotlib figure, or did you follow the advice from https://github.com/mne-tools/mne-python/issues/13242#issuecomment-2869683715 ? The advice there suggests to start with our code copy-pasted and trim it down. The reason for this is that we will probably end up with a minimal bit of code that causes the problem. I don't think we do anything fancy, it's probably "just" a matter of having the correct matplotlib calls and objects in the correct state to cause the issue, and if you only create a vanilla plot it might not have the right complexity/matplotlib calls to cause the problem.
@larsoner Got it, thanks for clarifying. I initially only tested with a vanilla matplotlib plot, but I’ll now follow your advice — copy the MNE plotting code from evoked.plot(), and trim it down until I have a minimal reproducible snippet that still causes the error. I’ll let you know soon!
Hi @larsoner ,
After testing, I can confirm this issue is not caused by Matplotlib itself. Plain Matplotlib plots save as SVG and PDF without any errors.
The error message:
AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'
occurs only when using MNE’s plotting functions (e.g., evoked.plot()) or code derived from them. This indicates the problem lies in MNE’s plotting/export logic and its interaction with specific Matplotlib backends.
From what I understand, this is likely due to interactive features or widget callbacks in MNE that depend on copy_from_bbox, which is not available on non-interactive backends, such as SVG or PDF. Such backends don’t support some interactive optimizations, causing this attribute error during save.
The original error was due to copy_from_bbox missing in SVG backend.
Patching SpanSelector.update_background and replacing copy_from_bbox with a dummy function solves the issue.
This confirms the problem lies in MNE’s use of matplotlib interactive features when exporting static formats.
Following the suggestion to isolate the problem, I’m working on creating a minimal reproducible example that uses MNE plotting code and still triggers this error. Please let me know if you would like me to proceed or if there are any specific steps you would like me to take next to isolate the issue with MNE’s plotting code.
Thanks!
Hi @larsoner,
I trimmed down from the MNE plotting code itself and ended up with this reproducible snippet (all directly from the codebase, no external dataset needed):
import mne, numpy as np
info = mne.create_info(['EEG 001', 'EEG 002'], 1000, ['eeg', 'eeg'])
data = np.random.randn(2, 1000) * 1e-6
evoked = mne.EvokedArray(data, info)
fig = evoked.plot(show=False)
fig.savefig("test_output.svg") # raises AttributeError
This always raises:
AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'
So the failure comes from evoked.plot() attaching interactive features (SpanSelector etc.) that depend on copy_from_bbox, which static backends like SVG/PDF don’t implement.
Let me know if this is what you needed, and what you’d like me to do next.
Thanks!
Okay next:
- Copy-paste into your script the MNE plotting code so you no longer need any
mne.vizfunctions (i.e., any functions you need should all be copied into the script, and the only remainingmne.calls/imports aremne.create_infoandmne.EvokedArray) - Ensure you still get the correct
FigureCanvasSVGerror (i.e., have copied all the code into the script correctly) - Remove / simplify some of the copied-in viz code, checking with each removal that 1) some plot still comes up, and 2) the correct
FigureCanvasSVGerror still occurs when you trysavefig. - Repeat (3) until you have the smallest snippet of code possible / have removed as much code as you can while still obtaining some
FigureCanvasSVGerror - If possible (and it should be), remove any
mne.calls completely, i.e., reduce it tomatplotlibandnumpycalls
Hey @larsoner
I followed all the steps mentioned and I came up with the smallest snippet of code possible that still produces the following error:
AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'
Also, I have completely removed all the mne. calls and reduced it to matplotlib and numpy calls.
Let me know to proceed or if any other changes are to be done! (ps. this was very fun to do ;)
Following is the code:
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from functools import partial
def _picks_to_idx(info, picks, none="data", exclude=()):
return list(range(len(info["ch_names"])))
_VALID_CHANNEL_TYPES = ["eeg", "meg", "grad", "mag", "eog", "ecg", "misc"]
def _line_plot_onselect(xmin, xmax, **kwargs):
pass
def plt_show(show=True):
"""Minimal placeholder for MNE's plt_show."""
if show:
import matplotlib.pyplot as plt
plt.show()
# Dummy replacements for MNE
class DummyInfo(dict):
def __init__(self, ch_names, ch_types):
super().__init__()
self["ch_names"] = ch_names
self._ch_types = ch_types
def get_channel_types(self, picks):
return [self._ch_types[p] for p in picks]
class DummyEvoked:
def __init__(self, data, info, nave=1):
self.data = data
self.info = info
self.nave = nave
def _plot_evoked(
evoked,
picks=None,
exclude="bads",
unit=True,
show=True,
ylim=None,
proj=False,
xlim="tight",
hline=None,
units=None,
scalings=None,
titles=None,
axes=None,
plot_type="butterfly",
gfp=False,
window_title=None,
spatial_colors=False,
selectable=True,
zorder="unsorted",
time_unit="s",
sphere=None,
*,
highlight=None,
draw=True,
):
info = evoked.info # tr
picks = _picks_to_idx(info, picks, none="all", exclude=())
bad_ch_idx = [] # WROTE EEG HERE
if len(exclude) > 0:
if isinstance(exclude, str) and exclude == "bads":
exclude = bad_ch_idx
picks = np.array([pick for pick in picks if pick not in exclude])
types = np.array(info.get_channel_types(picks), str)
ch_types_used = list()
for this_type in _VALID_CHANNEL_TYPES:
if this_type in types:
ch_types_used.append(this_type)
fig = None
if axes is None:
fig, axes = plt.subplots(len(ch_types_used), 1, layout="constrained")
if isinstance(axes, plt.Axes):
axes = [axes]
fig.set_size_inches(6.4, 2 + len(axes))
if plot_type == "butterfly":
_plot_lines(
evoked.data,
info,
picks,
fig,
axes,
spatial_colors,
unit,
units,
scalings,
hline,
gfp,
types,
zorder,
xlim,
ylim,
bad_ch_idx,
titles,
ch_types_used,
selectable,
False,
line_alpha=1.0,
nave=evoked.nave,
time_unit=time_unit,
sphere=sphere,
highlight=highlight,
)
plt.setp(axes, xlabel=f"Time ({time_unit})")
plt.setp(fig.axes[: len(ch_types_used) - 1], xlabel="")
if draw:
fig.canvas.draw()
plt_show(show)
return fig
def _plot_lines(
data,
info,
picks,
fig,
axes,
spatial_colors,
unit,
units,
scalings,
hline,
gfp,
types,
zorder,
xlim,
ylim,
bad_ch_idx,
titles,
ch_types_used,
selectable,
psd,
line_alpha,
nave,
time_unit,
sphere,
*,
highlight,
):
"""Plot data as butterfly plot."""
from matplotlib import pyplot as plt
from matplotlib.widgets import SpanSelector
if selectable:
selectables = np.ones(len(ch_types_used), dtype=bool)
if selectable:
for ax in np.array(axes)[selectables]:
if len(ax.lines) == 1:
continue
text = ax.annotate(
"Loading...",
xy=(0.01, 0.1),
xycoords="axes fraction",
fontsize=20,
color="green",
zorder=3,
)
text.set_visible(False)
callback_onselect = partial(
_line_plot_onselect,
ch_types=ch_types_used,
info=info,
data=data,
text=text,
psd=psd,
time_unit=time_unit,
sphere=sphere,
)
blit = False if plt.get_backend() == "MacOSX" else True
ax._span_selector = SpanSelector(
ax,
callback_onselect,
"horizontal",
useblit=blit,
props=dict(alpha=0.5, facecolor="red"),
)
# Dummy data
ch_names = ["EEG 001", "EEG 002", "EEG 003"]
ch_types = ["eeg", "eeg", "eeg"]
info = DummyInfo(ch_names, ch_types)
data = np.random.randn(len(ch_names), 1000)
evoked = DummyEvoked(data, info, nave=1)
fig = _plot_evoked(evoked)
fig.savefig("test_output.svg")
I think it can be reduced further... for example _picks_to_idx can be replaced just by np.arange(len(data)). Remove conditionals like if plot_type == "butterfly", it is in your reproducible example so you don't need the conditional.
Can you remove the SpanSelector entirely and still have it fail to savefig properly?
Can you remove all of the ch_types_used stuff and assume there is just one channel type (there is, EEG)?
Can you continue simplifying to remove DummyInfo and DummyEvoked and make it operate on ndarrays?
How many kwargs can you remove from the functions? For example scalings is no longer used at all so should be removed.
I did further changes and this is what I have come up with:
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
def _line_plot_onselect(xmin, xmax, **kwargs):
pass
def _plot_evoked(data, show=True):
picks = np.arange(len(data))
fig, ax = plt.subplots(1, 1, layout="constrained")
fig.set_size_inches(6.4, 2.5)
ax.plot(data[picks].T, alpha=1.0)
ax.set_title("Butterfly Plot")
ax.set_ylabel("Amplitude")
ax.set_xlabel("Time (s)")
# SpanSelector
text = ax.annotate(
"Loading...",
xy=(0.01, 0.1),
xycoords="axes fraction",
fontsize=12,
color="green",
zorder=3,
)
text.set_visible(False)
blit = False if plt.get_backend() == "MacOSX" else True
ax._span_selector = SpanSelector(
ax,
_line_plot_onselect,
"horizontal",
useblit=blit,
props=dict(alpha=0.5, facecolor="red"),
)
if show:
plt.show()
return fig
# Dummy EEG data
data = np.random.randn(3, 1000)
fig = _plot_evoked(data, show=False)
fig.savefig("test_output.svg")
Only the SpanSelector couldn't be removed entirely, as I think it triggers bbox bug with SVG.
Rest all the DummyInfo, DummyEvoked, _picks_to_idx, conditionals, ch_types_used, etc have been removed.
Let me know if there are any further changes that I can make.
It can actually be quite a bit simpler! I still get the error with this:
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
def _line_plot_onselect(xmin, xmax, **kwargs):
pass
fig, ax = plt.subplots()
ax.plot(np.random.randn(1000, 3))
ax._span_selector = SpanSelector(
ax,
_line_plot_onselect,
"horizontal",
useblit=True,
)
fig.savefig("test_output.svg")
Not sure if you could trim it even further... I determined that useblit=False makes the error go away so that's when I stopped trimming.
I think that snippet above is good enough to submit to matplotlib as a bug report! @AasmaGupta could you do that at https://github.com/matplotlib/matplotlib/issues ? Closing as this is a pure matplotlib issue!
just reported upstream!
I was simplifying the code further and came up with a smaller snippet. Thanks for reporting this issue as a bug further. Yes, this was a pure matplotlib issue! Thanks @larsoner and @drammock for the guidance!