Skip to content

Add blitting support to button widgets #23457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/users/next_whats_new/widget_blitting.rst
Original file line number Diff line number Diff line change
@@ -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.
94 changes: 82 additions & 12 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm that Button doesn't use copy_from_bbox/restore_region like the other two widgets because the button covers the entire axes anyways?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and it's strictly a rectangle, so copy_from_bbox/restore_region would effectively be directly overwritten immediately.

self.canvas.blit(self.ax.bbox)
else:
self.canvas.draw()

def on_clicked(self, func):
"""
Expand Down Expand Up @@ -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`
Expand All @@ -977,21 +987,22 @@ 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*.

Parameters
----------
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)

Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`.

Expand All @@ -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
Expand All @@ -1473,19 +1515,34 @@ 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")
for y, label in zip(ys, labels)]
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
Expand Down Expand Up @@ -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())

Expand All @@ -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:
Expand Down