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:
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: