From 1ff61cf55f7a31e1a062146bf3af99eaa25963b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 25 Jan 2023 16:19:09 -0500 Subject: [PATCH 1/3] FIX: only try to update blit caches if the canvas we expect Because we may switch the canvas to a different type (that may not support blitting) when saving, check it `_clear` that the current canvas is the canvas we expect when we update the blitting caches on the draw_event callback. Closes #25075 --- lib/matplotlib/tests/test_widgets.py | 40 ++++++++++++++++++++++++++++ lib/matplotlib/widgets.py | 8 +++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 53fd95526ef9..92b87a96b3d2 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1,4 +1,5 @@ import functools +import io from unittest import mock from matplotlib._api.deprecation import MatplotlibDeprecationWarning @@ -23,6 +24,45 @@ def ax(): return get_ax() +def test_save_blitted_widget_as_pdf(): + from matplotlib.widgets import CheckButtons, RadioButtons + from matplotlib.cbook import _get_running_interactive_framework + if _get_running_interactive_framework() not in ['headless', None]: + pytest.xfail("Callback exceptions are not raised otherwise.") + + fig, ax = plt.subplots( + nrows=2, ncols=2, figsize=(5, 2), width_ratios=[1, 2] + ) + default_rb = RadioButtons(ax[0, 0], ['Apples', 'Oranges']) + styled_rb = RadioButtons( + ax[0, 1], ['Apples', 'Oranges'], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + radio_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']} + ) + + default_cb = CheckButtons(ax[1, 0], ['Apples', 'Oranges'], + actives=[True, True]) + styled_cb = CheckButtons( + ax[1, 1], ['Apples', 'Oranges'], + actives=[True, True], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + frame_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}, + check_props={'color': ['darkred', 'darkorange']} + ) + + ax[0, 0].set_title('Default') + ax[0, 1].set_title('Stylized') + # force an Agg render + fig.canvas.draw() + # force a pdf save + with io.BytesIO() as result_after: + fig.savefig(result_after, format='pdf') + + @pytest.mark.parametrize('kwargs', [ dict(), dict(useblit=True, button=1), diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6315450a1573..03816de95430 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1088,7 +1088,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event): + if self.ignore(event) or self.canvas is not self.ax.figure.canvas: return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._checks) @@ -1700,7 +1700,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event): + if self.ignore(event) or self.canvas is not self.ax.figure.canvas: return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._buttons) @@ -1971,7 +1971,7 @@ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, def clear(self, event): """Internal event handler to clear the cursor.""" - if self.ignore(event): + if self.ignore(event) or self.canvas is not self.ax.figure.canvas: return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) @@ -2106,7 +2106,7 @@ def disconnect(self): def clear(self, event): """Clear the cursor.""" - if self.ignore(event): + if self.ignore(event) or self.canvas is not self.ax.figure.canvas: return if self.useblit: for canvas, info in self._canvas_infos.items(): From faa4816bfe7f299049f0c14802e7e85c3c180391 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 25 Jan 2023 17:13:42 -0500 Subject: [PATCH 2/3] FIX: handle the change detection differently for multicursor There is not a singular canvas --- lib/matplotlib/widgets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 03816de95430..5ae3a53c980b 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2106,10 +2106,16 @@ def disconnect(self): def clear(self, event): """Clear the cursor.""" - if self.ignore(event) or self.canvas is not self.ax.figure.canvas: + if self.ignore(event): return if self.useblit: for canvas, info in self._canvas_infos.items(): + # someone has switched the canvas on us! This happens if + # `savefig` needs to save to a format the previous backend did + # not support (e.g. saving a figure using an Agg based backend + # saved to a vector format). + if canvas is not canvas.figure.canvas: + continue info["background"] = canvas.copy_from_bbox(canvas.figure.bbox) def onmove(self, event): From 6bb208d5a9c46ded307c7baef4fd255cdbc80c46 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 9 Feb 2023 14:29:46 -0500 Subject: [PATCH 3/3] MNT: put changed canvas logic in private method with docstring --- lib/matplotlib/widgets.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5ae3a53c980b..dfc0450132c6 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -90,6 +90,22 @@ def ignore(self, event): """ return not self.active + def _changed_canvas(self): + """ + Someone has switched the canvas on us! + + This happens if `savefig` needs to save to a format the previous + backend did not support (e.g. saving a figure using an Agg based + backend saved to a vector format). + + Returns + ------- + bool + True if the canvas has been changed. + + """ + return self.canvas is not self.ax.figure.canvas + class AxesWidget(Widget): """ @@ -1088,7 +1104,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event) or self.canvas is not self.ax.figure.canvas: + if self.ignore(event) or self._changed_canvas(): return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._checks) @@ -1700,7 +1716,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, def _clear(self, event): """Internal event handler to clear the buttons.""" - if self.ignore(event) or self.canvas is not self.ax.figure.canvas: + if self.ignore(event) or self._changed_canvas(): return self._background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self._buttons) @@ -1971,7 +1987,7 @@ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, def clear(self, event): """Internal event handler to clear the cursor.""" - if self.ignore(event) or self.canvas is not self.ax.figure.canvas: + if self.ignore(event) or self._changed_canvas(): return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox)