Skip to content

Commit 212b0ff

Browse files
committed
Merge pull request matplotlib#4738 from tacaswell/fix_repl_ensure_fig_unstale
MNT: overhaul stale handling
2 parents 2ff12fb + 25079bf commit 212b0ff

File tree

10 files changed

+127
-51
lines changed

10 files changed

+127
-51
lines changed

examples/pylab_examples/system_monitor.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ def get_net():
2121
def get_stats():
2222
return get_memory(), get_cpu(), get_net()
2323

24-
25-
# turn interactive mode on for dynamic updates. If you aren't in
26-
# interactive mode, you'll need to use a GUI event handler/timer.
27-
plt.ion()
28-
2924
fig, ax = plt.subplots()
3025
ind = np.arange(1, 4)
26+
27+
# show the figure, but do not block
28+
plt.show(block=False)
29+
30+
3131
pm, pc, pn = plt.bar(ind, get_stats())
3232
centers = ind + 0.5*pm.get_width()
3333
pm.set_facecolor('r')
@@ -44,10 +44,21 @@ def get_stats():
4444
for i in range(200): # run for a little while
4545
m, c, n = get_stats()
4646

47+
# update the animated artists
4748
pm.set_height(m)
4849
pc.set_height(c)
4950
pn.set_height(n)
51+
52+
# ask the canvas to re-draw itself the next time it
53+
# has a chance.
54+
# For most of the GUI backends this adds an event to the queue
55+
# of the GUI frameworks event loop.
56+
fig.canvas.draw_idle()
5057
try:
58+
# make sure that the GUI framework has a chance to run its event loop
59+
# and clear any GUI events. This needs to be in a try/except block
60+
# because the default implemenation of this method is to raise
61+
# NotImplementedError
5162
fig.canvas.flush_events()
5263
except NotImplementedError:
5364
pass

lib/matplotlib/animation.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -1030,17 +1030,18 @@ def __init__(self, fig, artists, *args, **kwargs):
10301030

10311031
def _init_draw(self):
10321032
# Make all the artists involved in *any* frame invisible
1033-
axes = []
1033+
figs = set()
10341034
for f in self.new_frame_seq():
10351035
for artist in f:
10361036
artist.set_visible(False)
1037+
artist.set_animated(self._blit)
10371038
# Assemble a list of unique axes that need flushing
1038-
if artist.axes not in axes:
1039-
axes.append(artist.axes)
1039+
if artist.axes.figure not in figs:
1040+
figs.add(artist.axes.figure)
10401041

10411042
# Flush the needed axes
1042-
for ax in axes:
1043-
ax.figure.canvas.draw()
1043+
for fig in figs:
1044+
fig.canvas.draw()
10441045

10451046
def _pre_draw(self, framedata, blit):
10461047
'''
@@ -1155,6 +1156,8 @@ def _init_draw(self):
11551156
self._draw_frame(next(self.new_frame_seq()))
11561157
else:
11571158
self._drawn_artists = self._init_func()
1159+
for a in self._drawn_artists:
1160+
a.set_animated(self._blit)
11581161

11591162
def _draw_frame(self, framedata):
11601163
# Save the data for potential saving of movies.
@@ -1167,3 +1170,5 @@ def _draw_frame(self, framedata):
11671170
# Call the func with framedata and args. If blitting is desired,
11681171
# func needs to return a sequence of any artists that were modified.
11691172
self._drawn_artists = self._func(framedata, *self._args)
1173+
for a in self._drawn_artists:
1174+
a.set_animated(self._blit)

lib/matplotlib/artist.py

+30-20
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,9 @@ def draw_wrapper(artist, renderer, *args, **kwargs):
6868
return draw_wrapper
6969

7070

71-
def _stale_figure_callback(self):
72-
self.figure.stale = True
73-
74-
75-
def _stale_axes_callback(self):
76-
self.axes.stale = True
71+
def _stale_axes_callback(self, val):
72+
if self.axes:
73+
self.axes.stale = val
7774

7875

7976
class Artist(object):
@@ -87,6 +84,7 @@ class Artist(object):
8784

8885
def __init__(self):
8986
self._stale = True
87+
self.stale_callback = None
9088
self._axes = None
9189
self.figure = None
9290

@@ -124,6 +122,7 @@ def __getstate__(self):
124122
# remove the unpicklable remove method, this will get re-added on load
125123
# (by the axes) if the artist lives on an axes.
126124
d['_remove_method'] = None
125+
d['stale_callback'] = None
127126
return d
128127

129128
def remove(self):
@@ -222,7 +221,7 @@ def axes(self, new_axes):
222221

223222
self._axes = new_axes
224223
if new_axes is not None and new_axes is not self:
225-
self.add_callback(_stale_axes_callback)
224+
self.stale_callback = _stale_axes_callback
226225

227226
return new_axes
228227

@@ -236,15 +235,16 @@ def stale(self):
236235

237236
@stale.setter
238237
def stale(self, val):
239-
# only trigger call-back stack on being marked as 'stale'
240-
# when not already stale
241-
# the draw process will take care of propagating the cleaning
242-
# process
243-
if not (self._stale == val):
244-
self._stale = val
245-
# only trigger propagation if marking as stale
246-
if self._stale:
247-
self.pchanged()
238+
self._stale = val
239+
240+
# if the artist is animated it does not take normal part in the
241+
# draw stack and is not expected to be drawn as part of the normal
242+
# draw loop (when not saving) so do not propagate this change
243+
if self.get_animated():
244+
return
245+
246+
if val and self.stale_callback is not None:
247+
self.stale_callback(self, val)
248248

249249
def get_window_extent(self, renderer):
250250
"""
@@ -608,9 +608,19 @@ def set_figure(self, fig):
608608
609609
ACCEPTS: a :class:`matplotlib.figure.Figure` instance
610610
"""
611+
# if this is a no-op just return
612+
if self.figure is fig:
613+
return
614+
# if we currently have a figure (the case of both `self.figure`
615+
# and `fig` being none is taken care of above) we then user is
616+
# trying to change the figure an artist is associated with which
617+
# is not allowed for the same reason as adding the same instance
618+
# to more than one Axes
619+
if self.figure is not None:
620+
raise RuntimeError("Can not put single artist in "
621+
"more than one figure")
611622
self.figure = fig
612623
if self.figure and self.figure is not self:
613-
self.add_callback(_stale_figure_callback)
614624
self.pchanged()
615625
self.stale = True
616626

@@ -804,9 +814,9 @@ def set_animated(self, b):
804814
805815
ACCEPTS: [True | False]
806816
"""
807-
self._animated = b
808-
self.pchanged()
809-
self.stale = True
817+
if self._animated != b:
818+
self._animated = b
819+
self.pchanged()
810820

811821
def update(self, props):
812822
"""

lib/matplotlib/axes/_base.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def __setstate__(self, state):
475475
container = getattr(self, container_name)
476476
for artist in container:
477477
artist._remove_method = container.remove
478-
self.stale = True
478+
self._stale = True
479479

480480
def get_window_extent(self, *args, **kwargs):
481481
"""
@@ -2059,7 +2059,8 @@ def draw(self, renderer=None, inframe=False):
20592059
if not self.get_visible():
20602060
return
20612061
renderer.open_group('axes')
2062-
2062+
# prevent triggering call backs during the draw process
2063+
self._stale = True
20632064
locator = self.get_axes_locator()
20642065
if locator:
20652066
pos = locator(self, renderer)

lib/matplotlib/backend_bases.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
from __future__ import (absolute_import, division, print_function,
3636
unicode_literals)
37+
from contextlib import contextmanager
3738

3839
from matplotlib.externals import six
3940
from matplotlib.externals.six.moves import xrange
@@ -1690,6 +1691,13 @@ def __init__(self, figure):
16901691
self.mouse_grabber = None # the axes currently grabbing mouse
16911692
self.toolbar = None # NavigationToolbar2 will set me
16921693
self._is_saving = False
1694+
self._is_idle_drawing = False
1695+
1696+
@contextmanager
1697+
def _idle_draw_cntx(self):
1698+
self._is_idle_drawing = True
1699+
yield
1700+
self._is_idle_drawing = False
16931701

16941702
def is_saving(self):
16951703
"""
@@ -2012,7 +2020,9 @@ def draw_idle(self, *args, **kwargs):
20122020
"""
20132021
:meth:`draw` only if idle; defaults to draw but backends can overrride
20142022
"""
2015-
self.draw(*args, **kwargs)
2023+
if not self._is_idle_drawing:
2024+
with self._idle_draw_cntx():
2025+
self.draw(*args, **kwargs)
20162026

20172027
def draw_cursor(self, event):
20182028
"""

lib/matplotlib/figure.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
docstring.interpd.update(projection_names=get_projection_names())
5252

5353

54+
def _stale_figure_callback(self, val):
55+
if self.figure:
56+
self.figure.stale = val
57+
58+
5459
class AxesStack(Stack):
5560
"""
5661
Specialization of the Stack to handle all tracking of Axes in a Figure.
@@ -330,6 +335,7 @@ def __init__(self,
330335
xy=(0, 0), width=1, height=1,
331336
facecolor=facecolor, edgecolor=edgecolor,
332337
linewidth=linewidth)
338+
333339
self._set_artist_props(self.patch)
334340
self.patch.set_aa(False)
335341

@@ -543,6 +549,7 @@ def suptitle(self, t, **kwargs):
543549
sup.remove()
544550
else:
545551
self._suptitle = sup
552+
546553
self.stale = True
547554
return self._suptitle
548555

@@ -650,6 +657,8 @@ def figimage(self, X,
650657
self.set_size_inches(figsize, forward=True)
651658

652659
im = FigureImage(self, cmap, norm, xo, yo, origin, **kwargs)
660+
im.stale_callback = _stale_figure_callback
661+
653662
im.set_array(X)
654663
im.set_alpha(alpha)
655664
if norm is None:
@@ -910,6 +919,7 @@ def add_axes(self, *args, **kwargs):
910919
self.sca(a)
911920
a._remove_method = lambda ax: self.delaxes(ax)
912921
self.stale = True
922+
a.stale_callback = _stale_figure_callback
913923
return a
914924

915925
@docstring.dedent_interpd
@@ -999,6 +1009,7 @@ def add_subplot(self, *args, **kwargs):
9991009
self.sca(a)
10001010
a._remove_method = lambda ax: self.delaxes(ax)
10011011
self.stale = True
1012+
a.stale_callback = _stale_figure_callback
10021013
return a
10031014

10041015
def clf(self, keep_observers=False):
@@ -1046,8 +1057,10 @@ def draw(self, renderer):
10461057
# draw the figure bounding box, perhaps none for white figure
10471058
if not self.get_visible():
10481059
return
1049-
renderer.open_group('figure')
10501060

1061+
renderer.open_group('figure')
1062+
# prevent triggering call backs during the draw process
1063+
self._stale = True
10511064
if self.get_tight_layout() and self.axes:
10521065
try:
10531066
self.tight_layout(renderer, **self._tight_parameters)
@@ -1119,12 +1132,11 @@ def draw_composite():
11191132
dsu.sort(key=itemgetter(0))
11201133
for zorder, a, func, args in dsu:
11211134
func(*args)
1122-
a.stale = False
11231135

11241136
renderer.close_group('figure')
1137+
self.stale = False
11251138

11261139
self._cachedRenderer = renderer
1127-
self.stale = False
11281140
self.canvas.draw_event(renderer)
11291141

11301142
def draw_artist(self, a):
@@ -1274,6 +1286,7 @@ def text(self, x, y, s, *args, **kwargs):
12741286
def _set_artist_props(self, a):
12751287
if a != self:
12761288
a.set_figure(self)
1289+
a.stale_callback = _stale_figure_callback
12771290
a.set_transform(self.transFigure)
12781291

12791292
@docstring.dedent_interpd
@@ -1350,7 +1363,7 @@ def _gci(self):
13501363
return None
13511364

13521365
def __getstate__(self):
1353-
state = self.__dict__.copy()
1366+
state = super(Figure, self).__getstate__()
13541367
# the axobservers cannot currently be pickled.
13551368
# Additionally, the canvas cannot currently be pickled, but this has
13561369
# the benefit of meaning that a figure can be detached from one canvas,

lib/matplotlib/image.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ class _AxesImageBase(martist.Artist, cm.ScalarMappable):
7171
iterpnames = interpolations_names
7272
# <end unused keys>
7373

74+
def set_cmap(self, cmap):
75+
super(_AxesImageBase, self).set_cmap(cmap)
76+
self.stale = True
77+
78+
def set_norm(self, norm):
79+
super(_AxesImageBase, self).set_norm(norm)
80+
self.stale = True
81+
7482
def __str__(self):
7583
return "AxesImage(%g,%g;%gx%g)" % tuple(self.axes.bbox.bounds)
7684

@@ -835,12 +843,12 @@ def set_filterrad(self, s):
835843
def set_norm(self, norm):
836844
if self._A is not None:
837845
raise RuntimeError('Cannot change colors after loading data')
838-
cm.ScalarMappable.set_norm(self, norm)
846+
super(NonUniformImage, self).set_norm(self, norm)
839847

840848
def set_cmap(self, cmap):
841849
if self._A is not None:
842850
raise RuntimeError('Cannot change colors after loading data')
843-
cm.ScalarMappable.set_cmap(self, cmap)
851+
super(NonUniformImage, self).set_cmap(self, cmap)
844852

845853

846854
class PcolorImage(martist.Artist, cm.ScalarMappable):
@@ -1033,6 +1041,7 @@ def get_extent(self):
10331041
def set_data(self, A):
10341042
"""Set the image array."""
10351043
cm.ScalarMappable.set_array(self, cbook.safe_masked_invalid(A))
1044+
self.stale = True
10361045

10371046
def set_array(self, A):
10381047
"""Deprecated; use set_data for consistency with other image types."""

0 commit comments

Comments
 (0)