diff --git a/doc/users/next_whats_new/widget_blitting.rst b/doc/users/next_whats_new/widget_blitting.rst new file mode 100644 index 000000000000..129ce8953d6f --- /dev/null +++ b/doc/users/next_whats_new/widget_blitting.rst @@ -0,0 +1,7 @@ +Blitting in Button widgets +-------------------------- + +The `.Button`, `.CheckButtons`, and `.RadioButtons` widgets now support +blitting for faster rendering, on backends that support it, by passing +``useblit=True`` to the constructor. Blitting is enabled by default on +supported backends. diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index af1994ca42b3..6b85117f994d 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -152,7 +152,7 @@ class Button(AxesWidget): """ def __init__(self, ax, label, image=None, - color='0.85', hovercolor='0.95'): + color='0.85', hovercolor='0.95', *, useblit=True): """ Parameters ---------- @@ -167,6 +167,9 @@ def __init__(self, ax, label, image=None, The color of the button when not activated. hovercolor : color The color of the button when the mouse is over it. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. """ super().__init__(ax) @@ -177,6 +180,8 @@ def __init__(self, ax, label, image=None, horizontalalignment='center', transform=ax.transAxes) + self._useblit = useblit and self.canvas.supports_blit + self._observers = cbook.CallbackRegistry(signals=["clicked"]) self.connect_event('button_press_event', self._click) @@ -209,7 +214,11 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + self.ax.draw_artist(self.ax) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() def on_clicked(self, func): """ @@ -968,6 +977,7 @@ class CheckButtons(AxesWidget): ---------- ax : `~matplotlib.axes.Axes` The parent Axes for the widget. + labels : list of `.Text` rectangles : list of `.Rectangle` @@ -977,7 +987,7 @@ class CheckButtons(AxesWidget): each box, but have ``set_visible(False)`` when its box is not checked. """ - def __init__(self, ax, labels, actives=None): + def __init__(self, ax, labels, actives=None, *, useblit=True): """ Add check buttons to `matplotlib.axes.Axes` instance *ax*. @@ -985,13 +995,14 @@ def __init__(self, ax, labels, actives=None): ---------- ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - labels : list of str The labels of the check buttons. - actives : list of bool, optional The initial check states of the buttons. The list must have the same length as *labels*. If not given, all buttons are unchecked. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. """ super().__init__(ax) @@ -1002,6 +1013,9 @@ def __init__(self, ax, labels, actives=None): if actives is None: actives = [False] * len(labels) + self._useblit = useblit and self.canvas.supports_blit + self._background = None + ys = np.linspace(1, 0, len(labels)+2)[1:-1] text_size = mpl.rcParams["font.size"] / 2 @@ -1017,13 +1031,26 @@ def __init__(self, ax, labels, actives=None): self._crosses = ax.scatter( [0.15] * len(ys), ys, marker='x', linewidth=1, s=text_size**2, c=["k" if active else "none" for active in actives], - transform=ax.transAxes + transform=ax.transAxes, animated=self._useblit, ) self.connect_event('button_press_event', self._clicked) + if self._useblit: + self.connect_event('draw_event', self._clear) self._observers = cbook.CallbackRegistry(signals=["clicked"]) + def _clear(self, event): + """Internal event handler to clear the buttons.""" + if self.ignore(event): + return + self._background = self.canvas.copy_from_bbox(self.ax.bbox) + self.ax.draw_artist(self._crosses) + if hasattr(self, '_lines'): + for l1, l2 in self._lines: + self.ax.draw_artist(l1) + self.ax.draw_artist(l2) + def _clicked(self, event): if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: return @@ -1084,7 +1111,17 @@ def set_active(self, index): l2.set_visible(not l2.get_visible()) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + if self._background is not None: + self.canvas.restore_region(self._background) + self.ax.draw_artist(self._crosses) + if hasattr(self, "_lines"): + for l1, l2 in self._lines: + self.ax.draw_artist(l1) + self.ax.draw_artist(l2) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) @@ -1143,7 +1180,8 @@ def lines(self): current_status = self.get_status() lineparams = {'color': 'k', 'linewidth': 1.25, 'transform': self.ax.transAxes, - 'solid_capstyle': 'butt'} + 'solid_capstyle': 'butt', + 'animated': self._useblit} for i, y in enumerate(ys): x, y = 0.05, y - h / 2 l1 = Line2D([x, x + w], [y + h, y], **lineparams) @@ -1447,7 +1485,8 @@ class RadioButtons(AxesWidget): The label text of the currently selected button. """ - def __init__(self, ax, labels, active=0, activecolor='blue'): + def __init__(self, ax, labels, active=0, activecolor='blue', *, + useblit=True): """ Add radio buttons to an `~.axes.Axes`. @@ -1461,6 +1500,9 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): The index of the initially selected button. activecolor : color The color of the selected button. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. """ super().__init__(ax) self.activecolor = activecolor @@ -1473,6 +1515,9 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): ys = np.linspace(1, 0, len(labels) + 2)[1:-1] text_size = mpl.rcParams["font.size"] / 2 + self._useblit = useblit and self.canvas.supports_blit + self._background = None + self.labels = [ ax.text(0.25, y, label, transform=ax.transAxes, horizontalalignment="left", verticalalignment="center") @@ -1480,12 +1525,24 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): self._buttons = ax.scatter( [.15] * len(ys), ys, transform=ax.transAxes, s=text_size**2, c=[activecolor if i == active else "none" for i in range(len(ys))], - edgecolor="black") + edgecolor="black", animated=self._useblit) self.connect_event('button_press_event', self._clicked) + if self._useblit: + self.connect_event('draw_event', self._clear) self._observers = cbook.CallbackRegistry(signals=["clicked"]) + def _clear(self, event): + """Internal event handler to clear the buttons.""" + if self.ignore(event): + return + self._background = self.canvas.copy_from_bbox(self.ax.bbox) + self.ax.draw_artist(self._buttons) + if hasattr(self, "_circles"): + for circle in self._circles: + self.ax.draw_artist(circle) + def _clicked(self, event): if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: return @@ -1524,8 +1581,20 @@ def set_active(self, index): if hasattr(self, "_circles"): # Remove once circles is removed. for i, p in enumerate(self._circles): p.set_facecolor(self.activecolor if i == index else "none") + if self.drawon and self._useblit: + self.ax.draw_artist(p) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + if self._background is not None: + self.canvas.restore_region(self._background) + self.ax.draw_artist(self._buttons) + if hasattr(self, "_circles"): + for p in self._circles: + self.ax.draw_artist(p) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() + if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) @@ -1549,7 +1618,8 @@ def circles(self): circles = self._circles = [ Circle(xy=self._buttons.get_offsets()[i], edgecolor="black", facecolor=self._buttons.get_facecolor()[i], - radius=radius, transform=self.ax.transAxes) + radius=radius, transform=self.ax.transAxes, + animated=self._useblit) for i in range(len(self.labels))] self._buttons.set_visible(False) for circle in circles: