Skip to content

Fix drawing animated artists changed in selector callback #21342

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
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
57 changes: 57 additions & 0 deletions lib/matplotlib/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,63 @@ def test_span_selector_bound(direction):
assert tool._edge_handles.positions == handle_positions


@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_span_selector_animated_artists_callback():
"""Check that the animated artists changed in callbacks are updated."""
x = np.linspace(0, 2 * np.pi, 100)
values = np.sin(x)

fig, ax = plt.subplots()
(ln,) = ax.plot(x, values, animated=True)
(ln2, ) = ax.plot([], animated=True)

# spin the event loop to let the backend process any pending operations
# before drawing artists
# See blitting tutorial
plt.pause(0.1)
ax.draw_artist(ln)
fig.canvas.blit(fig.bbox)

def mean(vmin, vmax):
# Return mean of values in x between *vmin* and *vmax*
indmin, indmax = np.searchsorted(x, (vmin, vmax))
v = values[indmin:indmax].mean()
ln2.set_data(x, v)

span = widgets.SpanSelector(ax, mean, direction='horizontal',
onmove_callback=mean,
interactive=True,
drag_from_anywhere=True,
useblit=True)

# Add span selector and check that the line is draw after it was updated
# by the callback
press_data = [1, 2]
move_data = [2, 2]
do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
assert span._get_animated_artists() == (ln, ln2)
assert ln.stale is False
assert ln2.stale
assert ln2.get_ydata() == 0.9547335049088455
span.update()
assert ln2.stale is False

# Change span selector and check that the line is drawn/updated after its
# value was updated by the callback
press_data = [4, 2]
move_data = [5, 2]
release_data = [5, 2]
do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1)
do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1)
assert ln.stale is False
assert ln2.stale
assert ln2.get_ydata() == -0.9424150707548072
do_event(span, 'release', xdata=release_data[0],
ydata=release_data[1], button=1)
assert ln2.stale is False


def check_lasso_selector(**kwargs):
ax = get_ax()

Expand Down
31 changes: 27 additions & 4 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,18 @@ def set_active(self, active):
if active:
self.update_background(None)

def _get_animated_artists(self):
"""
Convenience method to get all animated artists of the figure containing
this widget, excluding those already present in self.artists.
The returned tuple is not sorted by 'z_order': z_order sorting is
valid only when considering all artists and not only a subset of all
artists.
"""
return tuple(a for ax_ in self.ax.get_figure().get_axes()
for a in ax_.get_children()
if a.get_animated() and a not in self.artists)

def update_background(self, event):
"""Force an update of the background."""
# If you add a call to `ignore` here, you'll want to check edge case:
Expand All @@ -1848,15 +1860,21 @@ def update_background(self, event):
# Make sure that widget artists don't get accidentally included in the
# background, by re-rendering the background if needed (and then
# re-re-rendering the canvas with the visible widget artists).
needs_redraw = any(artist.get_visible() for artist in self.artists)
# We need to remove all artists which will be drawn when updating
# the selector: if we have animated artists in the figure, it is safer
# to redrawn by default, in case they have updated by the callback
# zorder needs to be respected when redrawing
artists = sorted(self.artists + self._get_animated_artists(),
key=lambda a: a.get_zorder())
needs_redraw = any(artist.get_visible() for artist in artists)
with ExitStack() as stack:
if needs_redraw:
for artist in self.artists:
for artist in artists:
stack.enter_context(artist._cm_set(visible=False))
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
if needs_redraw:
for artist in self.artists:
for artist in artists:
self.ax.draw_artist(artist)

def connect_default_events(self):
Expand Down Expand Up @@ -1903,7 +1921,12 @@ def update(self):
self.canvas.restore_region(self.background)
else:
self.update_background(None)
for artist in self.artists:
# We need to draw all artists, which are not included in the
# background, therefore we also draw self._get_animated_artists()
# and we make sure that we respect z_order
artists = sorted(self.artists + self._get_animated_artists(),
key=lambda a: a.get_zorder())
for artist in artists:
self.ax.draw_artist(artist)
self.canvas.blit(self.ax.bbox)
else:
Expand Down