diff --git a/examples/pylab_examples/system_monitor.py b/examples/pylab_examples/system_monitor.py index 07110190a2c4..4d8c2dac24c5 100644 --- a/examples/pylab_examples/system_monitor.py +++ b/examples/pylab_examples/system_monitor.py @@ -21,13 +21,13 @@ def get_net(): def get_stats(): return get_memory(), get_cpu(), get_net() - -# turn interactive mode on for dynamic updates. If you aren't in -# interactive mode, you'll need to use a GUI event handler/timer. -plt.ion() - fig, ax = plt.subplots() ind = np.arange(1, 4) + +# show the figure, but do not block +plt.show(block=False) + + pm, pc, pn = plt.bar(ind, get_stats()) centers = ind + 0.5*pm.get_width() pm.set_facecolor('r') @@ -44,10 +44,21 @@ def get_stats(): for i in range(200): # run for a little while m, c, n = get_stats() + # update the animated artists pm.set_height(m) pc.set_height(c) pn.set_height(n) + + # ask the canvas to re-draw itself the next time it + # has a chance. + # For most of the GUI backends this adds an event to the queue + # of the GUI frameworks event loop. + fig.canvas.draw_idle() try: + # make sure that the GUI framework has a chance to run its event loop + # and clear any GUI events. This needs to be in a try/except block + # because the default implemenation of this method is to raise + # NotImplementedError fig.canvas.flush_events() except NotImplementedError: pass diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 99b6c4d8d23d..f4ec996f469c 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -971,17 +971,18 @@ def __init__(self, fig, artists, *args, **kwargs): def _init_draw(self): # Make all the artists involved in *any* frame invisible - axes = [] + figs = set() for f in self.new_frame_seq(): for artist in f: artist.set_visible(False) + artist.set_animated(self._blit) # Assemble a list of unique axes that need flushing - if artist.axes not in axes: - axes.append(artist.axes) + if artist.axes.figure not in figs: + figs.add(artist.axes.figure) # Flush the needed axes - for ax in axes: - ax.figure.canvas.draw() + for fig in figs: + fig.canvas.draw() def _pre_draw(self, framedata, blit): ''' @@ -1093,6 +1094,8 @@ def _init_draw(self): self._draw_frame(next(self.new_frame_seq())) else: self._drawn_artists = self._init_func() + for a in self._drawn_artists: + a.set_animated(self._blit) def _draw_frame(self, framedata): # Save the data for potential saving of movies. @@ -1105,3 +1108,5 @@ def _draw_frame(self, framedata): # Call the func with framedata and args. If blitting is desired, # func needs to return a sequence of any artists that were modified. self._drawn_artists = self._func(framedata, *self._args) + for a in self._drawn_artists: + a.set_animated(self._blit) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index e6d428fba997..010bcfc1daf3 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -68,12 +68,9 @@ def draw_wrapper(artist, renderer, *args, **kwargs): return draw_wrapper -def _stale_figure_callback(self): - self.figure.stale = True - - -def _stale_axes_callback(self): - self.axes.stale = True +def _stale_axes_callback(self, val): + if self.axes: + self.axes.stale = val class Artist(object): @@ -87,6 +84,7 @@ class Artist(object): def __init__(self): self._stale = True + self.stale_callback = None self._axes = None self.figure = None @@ -124,6 +122,7 @@ def __getstate__(self): # remove the unpicklable remove method, this will get re-added on load # (by the axes) if the artist lives on an axes. d['_remove_method'] = None + d['stale_callback'] = None return d def remove(self): @@ -222,7 +221,7 @@ def axes(self, new_axes): self._axes = new_axes if new_axes is not None and new_axes is not self: - self.add_callback(_stale_axes_callback) + self.stale_callback = _stale_axes_callback return new_axes @@ -236,15 +235,16 @@ def stale(self): @stale.setter def stale(self, val): - # only trigger call-back stack on being marked as 'stale' - # when not already stale - # the draw process will take care of propagating the cleaning - # process - if not (self._stale == val): - self._stale = val - # only trigger propagation if marking as stale - if self._stale: - self.pchanged() + self._stale = val + + # if the artist is animated it does not take normal part in the + # draw stack and is not expected to be drawn as part of the normal + # draw loop (when not saving) so do not propagate this change + if self.get_animated(): + return + + if val and self.stale_callback is not None: + self.stale_callback(self, val) def get_window_extent(self, renderer): """ @@ -608,9 +608,19 @@ def set_figure(self, fig): ACCEPTS: a :class:`matplotlib.figure.Figure` instance """ + # if this is a no-op just return + if self.figure is fig: + return + # if we currently have a figure (the case of both `self.figure` + # and `fig` being none is taken care of above) we then user is + # trying to change the figure an artist is associated with which + # is not allowed for the same reason as adding the same instance + # to more than one Axes + if self.figure is not None: + raise RuntimeError("Can not put single artist in " + "more than one figure") self.figure = fig if self.figure and self.figure is not self: - self.add_callback(_stale_figure_callback) self.pchanged() self.stale = True @@ -804,9 +814,9 @@ def set_animated(self, b): ACCEPTS: [True | False] """ - self._animated = b - self.pchanged() - self.stale = True + if self._animated != b: + self._animated = b + self.pchanged() def update(self, props): """ diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0b3f8b20a738..50afb7bbfcc3 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -475,7 +475,7 @@ def __setstate__(self, state): container = getattr(self, container_name) for artist in container: artist._remove_method = container.remove - self.stale = True + self._stale = True def get_window_extent(self, *args, **kwargs): """ @@ -2059,7 +2059,8 @@ def draw(self, renderer=None, inframe=False): if not self.get_visible(): return renderer.open_group('axes') - + # prevent triggering call backs during the draw process + self._stale = True locator = self.get_axes_locator() if locator: pos = locator(self, renderer) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c82e0f34850c..3a5fdb9b5efa 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -34,6 +34,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from contextlib import contextmanager from matplotlib.externals import six from matplotlib.externals.six.moves import xrange @@ -1690,6 +1691,13 @@ def __init__(self, figure): self.mouse_grabber = None # the axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me self._is_saving = False + self._is_idle_drawing = False + + @contextmanager + def _idle_draw_cntx(self): + self._is_idle_drawing = True + yield + self._is_idle_drawing = False def is_saving(self): """ @@ -2012,7 +2020,9 @@ def draw_idle(self, *args, **kwargs): """ :meth:`draw` only if idle; defaults to draw but backends can overrride """ - self.draw(*args, **kwargs) + if not self._is_idle_drawing: + with self._idle_draw_cntx(): + self.draw(*args, **kwargs) def draw_cursor(self, event): """ diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 073d6889f74a..4256a6d04503 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -51,6 +51,11 @@ docstring.interpd.update(projection_names=get_projection_names()) +def _stale_figure_callback(self, val): + if self.figure: + self.figure.stale = val + + class AxesStack(Stack): """ Specialization of the Stack to handle all tracking of Axes in a Figure. @@ -330,6 +335,7 @@ def __init__(self, xy=(0, 0), width=1, height=1, facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth) + self._set_artist_props(self.patch) self.patch.set_aa(False) @@ -543,6 +549,7 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup + self.stale = True return self._suptitle @@ -650,6 +657,8 @@ def figimage(self, X, self.set_size_inches(figsize, forward=True) im = FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) + im.stale_callback = _stale_figure_callback + im.set_array(X) im.set_alpha(alpha) if norm is None: @@ -909,6 +918,7 @@ def add_axes(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True + a.stale_callback = _stale_figure_callback return a @docstring.dedent_interpd @@ -997,6 +1007,7 @@ def add_subplot(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True + a.stale_callback = _stale_figure_callback return a def clf(self, keep_observers=False): @@ -1044,8 +1055,10 @@ def draw(self, renderer): # draw the figure bounding box, perhaps none for white figure if not self.get_visible(): return - renderer.open_group('figure') + renderer.open_group('figure') + # prevent triggering call backs during the draw process + self._stale = True if self.get_tight_layout() and self.axes: try: self.tight_layout(renderer, **self._tight_parameters) @@ -1117,12 +1130,11 @@ def draw_composite(): dsu.sort(key=itemgetter(0)) for zorder, a, func, args in dsu: func(*args) - a.stale = False renderer.close_group('figure') + self.stale = False self._cachedRenderer = renderer - self.stale = False self.canvas.draw_event(renderer) def draw_artist(self, a): @@ -1272,6 +1284,7 @@ def text(self, x, y, s, *args, **kwargs): def _set_artist_props(self, a): if a != self: a.set_figure(self) + a.stale_callback = _stale_figure_callback a.set_transform(self.transFigure) @docstring.dedent_interpd @@ -1348,7 +1361,7 @@ def _gci(self): return None def __getstate__(self): - state = self.__dict__.copy() + state = super(Figure, self).__getstate__() # the axobservers cannot currently be pickled. # Additionally, the canvas cannot currently be pickled, but this has # the benefit of meaning that a figure can be detached from one canvas, diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 01e36e831785..bd5d459193d4 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -64,6 +64,14 @@ class _AxesImageBase(martist.Artist, cm.ScalarMappable): interpnames = list(six.iterkeys(_interpd)) + def set_cmap(self, cmap): + super(_AxesImageBase, self).set_cmap(cmap) + self.stale = True + + def set_norm(self, norm): + super(_AxesImageBase, self).set_norm(norm) + self.stale = True + def __str__(self): return "AxesImage(%g,%g;%gx%g)" % tuple(self.axes.bbox.bounds) @@ -828,12 +836,12 @@ def set_filterrad(self, s): def set_norm(self, norm): if self._A is not None: raise RuntimeError('Cannot change colors after loading data') - cm.ScalarMappable.set_norm(self, norm) + super(NonUniformImage, self).set_norm(self, norm) def set_cmap(self, cmap): if self._A is not None: raise RuntimeError('Cannot change colors after loading data') - cm.ScalarMappable.set_cmap(self, cmap) + super(NonUniformImage, self).set_cmap(self, cmap) class PcolorImage(martist.Artist, cm.ScalarMappable): @@ -1026,6 +1034,7 @@ def get_extent(self): def set_data(self, A): """Set the image array.""" cm.ScalarMappable.set_array(self, cbook.safe_masked_invalid(A)) + self.stale = True def set_array(self, A): """Deprecated; use set_data for consistency with other image types.""" diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 932babbde485..a35403c9e369 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -187,6 +187,14 @@ def set_figure(self, fig): for c in self.get_children(): c.set_figure(fig) + @martist.Artist.axes.setter + def axes(self, ax): + # TODO deal with this better + martist.Artist.axes.fset(self, ax) + for c in self.get_children(): + if c is not None: + c.axes = ax + def contains(self, mouseevent): for c in self.get_children(): a, b = c.contains(mouseevent) @@ -663,6 +671,11 @@ def add_artist(self, a): self._children.append(a) if not a.is_transform_set(): a.set_transform(self.get_transform()) + if self.axes is not None: + a.axes = self.axes + fig = self.figure + if fig is not None: + a.set_figure(fig) def draw(self, renderer): """ @@ -1041,6 +1054,8 @@ def __init__(self, loc, def set_child(self, child): "set the child to be anchored" self._child = child + if child is not None: + child.axes = self.axes self.stale = True def get_child(self): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index fd1a1d78ad81..0d13caf64f19 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -529,15 +529,16 @@ def make_active(event): figManager._cidgcf = cid _pylab_helpers.Gcf.set_active(figManager) - figManager.canvas.figure.number = num + fig = figManager.canvas.figure + fig.number = num if _INSTALL_FIG_OBSERVER: - figManager.canvas.figure.add_callback(_auto_draw_if_interactive) + fig.stale_callback = _auto_draw_if_interactive return figManager.canvas.figure -def _auto_draw_if_interactive(fig): +def _auto_draw_if_interactive(fig, val): """ This is an internal helper function for making sure that auto-redrawing works as intended in the plain python repl. @@ -547,7 +548,7 @@ def _auto_draw_if_interactive(fig): fig : Figure A figure object which is assumed to be associated with a canvas """ - if fig.stale and matplotlib.is_interactive(): + if val and matplotlib.is_interactive(): fig.canvas.draw_idle() diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index f8acede1f16a..9d43242ed9f5 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -163,19 +163,20 @@ def add_offsetboxes(ax, size=10, margin=.1, color='black'): """ Surround ax with OffsetBoxes """ - da = DrawingArea(size, size) - background = Rectangle((0, 0), width=size, - height=size, - facecolor=color, - edgecolor='None', - linewidth=0, - antialiased=False) - da.add_artist(background) m, mp = margin, 1+margin anchor_points = [(-m, -m), (-m, .5), (-m, mp), (mp, .5), (.5, mp), (mp, mp), (.5, -m), (mp, -m), (.5, -m)] for point in anchor_points: + da = DrawingArea(size, size) + background = Rectangle((0, 0), width=size, + height=size, + facecolor=color, + edgecolor='None', + linewidth=0, + antialiased=False) + da.add_artist(background) + anchored_box = AnchoredOffsetbox( loc=10, child=da,