Skip to content

Export as vector based graphics (SVG, EPS, PDF) throws an error for Evoked.plot() #13242

Closed

Description

@sjentsch

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

Activity

  1. agramfort commented on May 11, 2025

    @agramfort
    SponsorMember
  2. AasmaGupta commented on Aug 5, 2025

    @AasmaGupta

    Hey, I'd like to work on this issue. Can you assign me this?

  3. drammock commented on Aug 5, 2025

    @drammock
    Member

    @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.

  4. AasmaGupta commented on Aug 5, 2025

    @AasmaGupta

    @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.

  5. larsoner commented on Aug 7, 2025

    @larsoner
    Member

    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 #13242 (comment) ? 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.

  6. AasmaGupta commented on Aug 11, 2025

    @AasmaGupta

    @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!

  7. AasmaGupta commented on Aug 11, 2025

    @AasmaGupta

    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!

  8. AasmaGupta commented on Aug 20, 2025

    @AasmaGupta

    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!

  9. larsoner commented on Aug 20, 2025

    @larsoner
    Member

    Okay next:

    1. Copy-paste into your script the MNE plotting code so you no longer need any mne.viz functions (i.e., any functions you need should all be copied into the script, and the only remaining mne. calls/imports are mne.create_info and mne.EvokedArray)
    2. Ensure you still get the correct FigureCanvasSVG error (i.e., have copied all the code into the script correctly)
    3. Remove / simplify some of the copied-in viz code, checking with each removal that 1) some plot still comes up, and 2) the correct FigureCanvasSVG error still occurs when you try savefig.
    4. Repeat (3) until you have the smallest snippet of code possible / have removed as much code as you can while still obtaining some FigureCanvasSVG error
    5. If possible (and it should be), remove any mne. calls completely, i.e., reduce it to matplotlib and numpy calls
  10. AasmaGupta commented on Aug 28, 2025

    @AasmaGupta

    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")
    
    
    
    
  11. larsoner commented on Aug 28, 2025

    @larsoner
    Member

    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.

  12. AasmaGupta commented on Aug 29, 2025

    @AasmaGupta

    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.

  13. drammock commented on Aug 29, 2025

    @drammock
    Member

    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.

  14. larsoner commented on Aug 29, 2025

    @larsoner
    Member

    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!

  15. drammock commented on Aug 29, 2025

    @drammock
    Member

    just reported upstream!

  16. AasmaGupta commented on Aug 29, 2025

    @AasmaGupta

    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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions