🌐 AI搜索 & 代理 主页
Skip to content

[Bug]: figures with SpanSelector(..., useblit=True) can't be saved to SVG or PDF #30485

@drammock

Description

@drammock

Bug summary

a SpanSelector created with useblit=True causes an error when trying to save the figure as SVG. Reproducer is below, though I haven't tested all backends (only Agg and qtagg; both fail) or all output formats (only SVG, PDF, and PNG; PNG works and the other two don't). useblit=False allows SVG and PDF saving to work.

first reported at mne-tools/mne-python#13242 by @AasmaGupta, who did much of the initial isolation work. Tested on MPL 3.10.1 on Linux.

Code for reproduction

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector


def onselect(xmin, xmax, **kwargs):
    pass


fig, ax = plt.subplots()
ax.plot(np.random.randn(100, 3))
span_selector = SpanSelector(
    ax,
    onselect,
    "horizontal",
    useblit=True,
)

fig.savefig("test.svg")  # 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'

Actual outcome

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/Desktop/svgmwe.py:22
     14 ax.plot(np.random.randn(100, 3))
     15 span_selector = SpanSelector(
     16     ax,
     17     onselect,
     18     "horizontal",
     19     useblit=True,
     20 )
---> 22 fig.savefig("test.svg")  # 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/figure.py:3490, in Figure.savefig(self, fname, transparent, **kwargs)
   3488     for ax in self.axes:
   3489         _recursively_make_axes_transparent(stack, ax)
-> 3490 self.canvas.print_figure(fname, **kwargs)

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/backend_bases.py:2184, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2180 try:
   2181     # _get_renderer may change the figure dpi (as vector formats
   2182     # force the figure dpi to 72), so we need to set it again here.
   2183     with cbook._setattr_cm(self.figure, dpi=dpi):
-> 2184         result = print_method(
   2185             filename,
   2186             facecolor=facecolor,
   2187             edgecolor=edgecolor,
   2188             orientation=orientation,
   2189             bbox_inches_restore=_bbox_inches_restore,
   2190             **kwargs)
   2191 finally:
   2192     if bbox_inches and restore_bbox:

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/backend_bases.py:2040, in FigureCanvasBase._switch_canvas_and_return_print_method.<locals>.<lambda>(*args, **kwargs)
   2036     optional_kws = {  # Passed by print_figure for other renderers.
   2037         "dpi", "facecolor", "edgecolor", "orientation",
   2038         "bbox_inches_restore"}
   2039     skip = optional_kws - {*inspect.signature(meth).parameters}
-> 2040     print_method = functools.wraps(meth)(lambda *args, **kwargs: meth(
   2041         *args, **{k: v for k, v in kwargs.items() if k not in skip}))
   2042 else:  # Let third-parties do as they see fit.
   2043     print_method = meth

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/backends/backend_svg.py:1351, in FigureCanvasSVG.print_svg(self, filename, bbox_inches_restore, metadata)
   1346 w, h = width * 72, height * 72
   1347 renderer = MixedModeRenderer(
   1348     self.figure, width, height, dpi,
   1349     RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata),
   1350     bbox_inches_restore=bbox_inches_restore)
-> 1351 self.figure.draw(renderer)
   1352 renderer.finalize()

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/figure.py:3264, in Figure.draw(self, renderer)
   3261 finally:
   3262     self.stale = False
-> 3264 DrawEvent("draw_event", self.canvas, renderer)._process()

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/backend_bases.py:1189, in Event._process(self)
   1187 def _process(self):
   1188     """Process this event on ``self.canvas``, then unset ``guiEvent``."""
-> 1189     self.canvas.callbacks.process(self.name, self)
   1190     self.guiEvent = None

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/cbook.py:366, in CallbackRegistry.process(self, s, *args, **kwargs)
    364 except Exception as exc:
    365     if self.exception_handler is not None:
--> 366         self.exception_handler(exc)
    367     else:
    368         raise

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/cbook.py:110, in _exception_printer(exc)
    108 def _exception_printer(exc):
    109     if _get_running_interactive_framework() in ["headless", None]:
--> 110         raise exc
    111     else:
    112         traceback.print_exc()

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/cbook.py:361, in CallbackRegistry.process(self, s, *args, **kwargs)
    359 if func is not None:
    360     try:
--> 361         func(*args, **kwargs)
    362     # this does not capture KeyboardInterrupt, SystemExit,
    363     # and GeneratorExit
    364     except Exception as exc:

File /opt/mambaforge/envs/mnedev/lib/python3.13/site-packages/matplotlib/widgets.py:2167, in _SelectorWidget.update_background(self, event)
   2165             stack.enter_context(artist._cm_set(visible=False))
   2166         self.canvas.draw()
-> 2167     self.background = self.canvas.copy_from_bbox(self.ax.bbox)
   2168 if needs_redraw:
   2169     for artist in artists:

AttributeError: 'FigureCanvasSVG' object has no attribute 'copy_from_bbox'

Expected outcome

Figure is saved normally.

Additional information

No response

Operating system

Linux

Matplotlib Version

3.10.1

Matplotlib Backend

Agg and QtAgg

Python version

3.13.2

Jupyter version

n/a

Installation

conda

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions