From 02158b1b9383e6e25185e14a375e3f0c10048c50 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 17 Jul 2015 21:28:13 -0400 Subject: [PATCH 01/30] FIX: attempting to draw marks figure as not-stale Closes #4732 --- lib/matplotlib/figure.py | 132 ++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 073d6889f74a..f0db08182d7a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1045,86 +1045,88 @@ def draw(self, renderer): if not self.get_visible(): return renderer.open_group('figure') + try: + if self.get_tight_layout() and self.axes: + try: + self.tight_layout(renderer, **self._tight_parameters) + except ValueError: + pass + # ValueError can occur when resizing a window. - if self.get_tight_layout() and self.axes: - try: - self.tight_layout(renderer, **self._tight_parameters) - except ValueError: - pass - # ValueError can occur when resizing a window. - - if self.frameon: - self.patch.draw(renderer) - - # a list of (zorder, func_to_call, list_of_args) - dsu = [] - - for a in self.patches: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) - - for a in self.lines: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) - - for a in self.artists: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + if self.frameon: + self.patch.draw(renderer) - # override the renderer default if self.suppressComposite - # is not None - not_composite = renderer.option_image_nocomposite() - if self.suppressComposite is not None: - not_composite = self.suppressComposite + # a list of (zorder, func_to_call, list_of_args) + dsu = [] - if (len(self.images) <= 1 or not_composite or - not cbook.allequal([im.origin for im in self.images])): - for a in self.images: + for a in self.patches: dsu.append((a.get_zorder(), a, a.draw, [renderer])) - else: - # make a composite image blending alpha - # list of (_image.Image, ox, oy) - mag = renderer.get_image_magnification() - ims = [(im.make_image(mag), im.ox, im.oy, im.get_alpha()) - for im in self.images] - im = _image.from_images(int(self.bbox.height * mag), - int(self.bbox.width * mag), - ims) - - im.is_grayscale = False - l, b, w, h = self.bbox.bounds - - def draw_composite(): - gc = renderer.new_gc() - gc.set_clip_rectangle(self.bbox) - gc.set_clip_path(self.get_clip_path()) - renderer.draw_image(gc, l, b, im) - gc.restore() + for a in self.lines: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - dsu.append((self.images[0].get_zorder(), self.images[0], - draw_composite, [])) + for a in self.artists: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - # render the axes - for a in self.axes: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + # override the renderer default if self.suppressComposite + # is not None + not_composite = renderer.option_image_nocomposite() + if self.suppressComposite is not None: + not_composite = self.suppressComposite - # render the figure text - for a in self.texts: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + if (len(self.images) <= 1 or not_composite or + not cbook.allequal([im.origin for im in self.images])): + for a in self.images: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) + else: + # make a composite image blending alpha + # list of (_image.Image, ox, oy) + mag = renderer.get_image_magnification() + ims = [(im.make_image(mag), im.ox, im.oy, im.get_alpha()) + for im in self.images] + + im = _image.from_images(int(self.bbox.height * mag), + int(self.bbox.width * mag), + ims) + + im.is_grayscale = False + l, b, w, h = self.bbox.bounds + + def draw_composite(): + gc = renderer.new_gc() + gc.set_clip_rectangle(self.bbox) + gc.set_clip_path(self.get_clip_path()) + renderer.draw_image(gc, l, b, im) + gc.restore() + + dsu.append((self.images[0].get_zorder(), self.images[0], + draw_composite, [])) + + # render the axes + for a in self.axes: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - for a in self.legends: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + # render the figure text + for a in self.texts: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - dsu = [row for row in dsu if not row[1].get_animated()] - dsu.sort(key=itemgetter(0)) - for zorder, a, func, args in dsu: - func(*args) - a.stale = False + for a in self.legends: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - renderer.close_group('figure') + dsu = [row for row in dsu if not row[1].get_animated()] + dsu.sort(key=itemgetter(0)) + for zorder, a, func, args in dsu: + func(*args) + a.stale = False + finally: + renderer.close_group('figure') + self.stale = False self._cachedRenderer = renderer - self.stale = False self.canvas.draw_event(renderer) + + def draw_artist(self, a): """ draw :class:`matplotlib.artist.Artist` instance *a* only -- From b0c37415f112101783bb20f22883d2bc392aac2b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 18 Jul 2015 02:11:28 -0400 Subject: [PATCH 02/30] PEP: remove extra lines --- lib/matplotlib/figure.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f0db08182d7a..aeb7ffef300f 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1125,8 +1125,6 @@ def draw_composite(): self._cachedRenderer = renderer self.canvas.draw_event(renderer) - - def draw_artist(self, a): """ draw :class:`matplotlib.artist.Artist` instance *a* only -- From 7f3ef65e79b958692fc2f480d64d7800d24806cd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 18:17:04 -0400 Subject: [PATCH 03/30] FIX: do not propogate stale on animated artists --- lib/matplotlib/artist.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index e6d428fba997..e0ab07bdc898 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -236,6 +236,13 @@ def stale(self): @stale.setter def stale(self, val): + # if the artist is animated it does not take normal part in the + # draw stack and is expected to be drawn as part of the normal + # draw loop (when not saving) so do not propagate this change + if self.get_animated(): + self._stale = val + return + # only trigger call-back stack on being marked as 'stale' # when not already stale # the draw process will take care of propagating the cleaning From a6776344f9d6611d40576602e28511f2e8174673 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 18:53:19 -0400 Subject: [PATCH 04/30] MNT: Limit what propagates stale to figure Only propagate stale state to the figure from it's direct children, not all artists. This is to prevent double call backs when using interactive mode with backends which do not implement an asynchronous draw_idle. --- lib/matplotlib/artist.py | 5 ----- lib/matplotlib/figure.py | 11 +++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index e0ab07bdc898..7c97e02e44a9 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -68,10 +68,6 @@ 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 @@ -617,7 +613,6 @@ def set_figure(self, fig): """ self.figure = fig if self.figure and self.figure is not self: - self.add_callback(_stale_figure_callback) self.pchanged() self.stale = True diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index aeb7ffef300f..592784aa2d86 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -51,6 +51,10 @@ docstring.interpd.update(projection_names=get_projection_names()) +def _stale_figure_callback(self): + self.figure.stale = True + + class AxesStack(Stack): """ Specialization of the Stack to handle all tracking of Axes in a Figure. @@ -330,6 +334,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 +548,7 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup + self.stale = True return self._suptitle @@ -650,6 +656,8 @@ def figimage(self, X, self.set_size_inches(figsize, forward=True) im = FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) + im.add_callback(_stale_figure_callback) + im.set_array(X) im.set_alpha(alpha) if norm is None: @@ -909,6 +917,7 @@ def add_axes(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True + a.add_callback(_stale_figure_callback) return a @docstring.dedent_interpd @@ -997,6 +1006,7 @@ def add_subplot(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True + a.add_callback(_stale_figure_callback) return a def clf(self, keep_observers=False): @@ -1272,6 +1282,7 @@ def text(self, x, y, s, *args, **kwargs): def _set_artist_props(self, a): if a != self: a.set_figure(self) + a.add_callback(_stale_figure_callback) a.set_transform(self.transFigure) @docstring.dedent_interpd From 6d6e39a81ba6f49be584d308f29fd4139d86f1da Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 18:58:25 -0400 Subject: [PATCH 05/30] API: forbid changing figure of Artist instance This is to match the behavior of artists being uniquely associated with a single Axes. --- lib/matplotlib/artist.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7c97e02e44a9..7928a39b56f5 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -611,6 +611,17 @@ 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.pchanged() From 1aec54b16e02427ea60e2ba3dd450d967bb7e418 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 19:19:08 -0400 Subject: [PATCH 06/30] MNT: do not re-trigger draws during draw If a draw has been forced, but either the figure or axes is not stale some of the draw processes will cause them to become scale (because the current implementation is naive and does no check if values have changed so no-op sets still mark the artist as stale). By setting _stale to be True the stale events will not propagate past the axes/figure and schedule another redraw. This only really matters for Canvas classes which do not implement an asynchronous `draw_idle`. If there is an asynchronous `draw_idle` the additional draw requests will be ignored until the current draw is finished anyway. --- lib/matplotlib/axes/_base.py | 3 ++- lib/matplotlib/figure.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0b3f8b20a738..4fc76bcfbafb 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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/figure.py b/lib/matplotlib/figure.py index 592784aa2d86..b02dd8a468e9 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1054,7 +1054,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') + # prevent triggering call backs during the draw process + self._stale = True try: if self.get_tight_layout() and self.axes: try: From 1a58f173b8e21335ea92a2c26435c36dda786baa Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 19:40:35 -0400 Subject: [PATCH 07/30] DOC: added explanatory comments to example --- examples/pylab_examples/system_monitor.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/examples/pylab_examples/system_monitor.py b/examples/pylab_examples/system_monitor.py index 07110190a2c4..964e4bcedb66 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 frame works event loop. + fig.canvas.draw_idle() try: + # make sure that the GUI framework has a chance to run it's event loop + # and clear and 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 From 72c5ce21e966dc6c5c52bd95e2c54666d5e0df92 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 30 Jul 2015 21:09:09 -0400 Subject: [PATCH 08/30] FIX: make sure that offset boxes propagate axes When setting the axes of an offset box to propagate to it's children. --- lib/matplotlib/offsetbox.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 932babbde485..ed5b9d6effac 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -187,6 +187,13 @@ 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(): + c.axes = ax + def contains(self, mouseevent): for c in self.get_children(): a, b = c.contains(mouseevent) @@ -663,6 +670,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 +1053,7 @@ def __init__(self, loc, def set_child(self, child): "set the child to be anchored" self._child = child + child.axes = self.axes self.stale = True def get_child(self): From 4cc9a7479c5b59bab13538ab98e8246ec2fb53cd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 30 Jul 2015 23:18:49 -0400 Subject: [PATCH 09/30] TST: don't reuse artists in tests --- lib/matplotlib/tests/test_tightlayout.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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, From b39fd90554e13bc2eee26cc84aaa0f6e952f109a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 30 Jul 2015 23:50:45 -0400 Subject: [PATCH 10/30] FIX: protect against None child --- lib/matplotlib/offsetbox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index ed5b9d6effac..a35403c9e369 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -192,7 +192,8 @@ def axes(self, ax): # TODO deal with this better martist.Artist.axes.fset(self, ax) for c in self.get_children(): - c.axes = ax + if c is not None: + c.axes = ax def contains(self, mouseevent): for c in self.get_children(): @@ -1053,7 +1054,8 @@ def __init__(self, loc, def set_child(self, child): "set the child to be anchored" self._child = child - child.axes = self.axes + if child is not None: + child.axes = self.axes self.stale = True def get_child(self): From 0b49a55c1959ec6ec051c2a2a315a06a1b141f62 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 00:04:52 -0400 Subject: [PATCH 11/30] MNT: use direct callback for stale, not pchanged Do not re-use a much too broad callback registry for propagating stale state. --- lib/matplotlib/artist.py | 21 ++++++++------------- lib/matplotlib/figure.py | 12 ++++++------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7928a39b56f5..79f5620828bf 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -68,7 +68,7 @@ def draw_wrapper(artist, renderer, *args, **kwargs): return draw_wrapper -def _stale_axes_callback(self): +def _stale_axes_callback(self, value): self.axes.stale = True @@ -83,6 +83,7 @@ class Artist(object): def __init__(self): self._stale = True + self.stale_callback = None self._axes = None self.figure = None @@ -218,7 +219,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 @@ -232,22 +233,16 @@ def stale(self): @stale.setter def stale(self, val): + self._stale = val + # if the artist is animated it does not take normal part in the - # draw stack and is expected to be drawn as part of the normal + # 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(): - self._stale = val return - # 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() + if self.stale_callback is not None: + self.stale_callback(self, val) def get_window_extent(self, renderer): """ diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index b02dd8a468e9..f35ba640911d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -51,8 +51,8 @@ docstring.interpd.update(projection_names=get_projection_names()) -def _stale_figure_callback(self): - self.figure.stale = True +def _stale_figure_callback(self, val): + self.figure.stale = val class AxesStack(Stack): @@ -656,7 +656,7 @@ def figimage(self, X, self.set_size_inches(figsize, forward=True) im = FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) - im.add_callback(_stale_figure_callback) + im.stale_callback = _stale_figure_callback im.set_array(X) im.set_alpha(alpha) @@ -917,7 +917,7 @@ def add_axes(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True - a.add_callback(_stale_figure_callback) + a.stale_callback = _stale_figure_callback return a @docstring.dedent_interpd @@ -1006,7 +1006,7 @@ def add_subplot(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) self.stale = True - a.add_callback(_stale_figure_callback) + a.stale_callback = _stale_figure_callback return a def clf(self, keep_observers=False): @@ -1285,7 +1285,7 @@ def text(self, x, y, s, *args, **kwargs): def _set_artist_props(self, a): if a != self: a.set_figure(self) - a.add_callback(_stale_figure_callback) + a.stale_callback = _stale_figure_callback a.set_transform(self.transFigure) @docstring.dedent_interpd From 59eaad65210bfa00641ff18b1e3283f175057146 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 00:10:27 -0400 Subject: [PATCH 12/30] MNT: still only propagate True up the chain --- lib/matplotlib/artist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 79f5620828bf..396e8dc4692a 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -241,7 +241,7 @@ def stale(self, val): if self.get_animated(): return - if self.stale_callback is not None: + if val and self.stale_callback is not None: self.stale_callback(self, val) def get_window_extent(self, renderer): From 69b1b365006b47cd9f2f913e520dc9b34d3c4756 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 00:14:55 -0400 Subject: [PATCH 13/30] MNT: fix vanilla python repl hook --- lib/matplotlib/pyplot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index fd1a1d78ad81..2cb872d44055 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 + return fig -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() From 770b8d54b14fd2aa1c84f20914ad5b3dcb82601e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 00:55:47 -0400 Subject: [PATCH 14/30] FIX: remove excessive stale cleaning --- lib/matplotlib/figure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f35ba640911d..a745ba012e75 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1130,7 +1130,6 @@ def draw_composite(): dsu.sort(key=itemgetter(0)) for zorder, a, func, args in dsu: func(*args) - a.stale = False finally: renderer.close_group('figure') self.stale = False From aa810b69c80fc4da6e6a19e6f3a1532f5d17d1e1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 00:56:55 -0400 Subject: [PATCH 15/30] MNT: don't need try/except anymore Always propagate stale up (even if immediate parent is stale) --- lib/matplotlib/figure.py | 133 +++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index a745ba012e75..92c95d654e9c 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1058,81 +1058,80 @@ def draw(self, renderer): renderer.open_group('figure') # prevent triggering call backs during the draw process self._stale = True - try: - if self.get_tight_layout() and self.axes: - try: - self.tight_layout(renderer, **self._tight_parameters) - except ValueError: - pass - # ValueError can occur when resizing a window. + if self.get_tight_layout() and self.axes: + try: + self.tight_layout(renderer, **self._tight_parameters) + except ValueError: + pass + # ValueError can occur when resizing a window. - if self.frameon: - self.patch.draw(renderer) + if self.frameon: + self.patch.draw(renderer) - # a list of (zorder, func_to_call, list_of_args) - dsu = [] + # a list of (zorder, func_to_call, list_of_args) + dsu = [] - for a in self.patches: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + for a in self.patches: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - for a in self.lines: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + for a in self.lines: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - for a in self.artists: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) + for a in self.artists: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) - # override the renderer default if self.suppressComposite - # is not None - not_composite = renderer.option_image_nocomposite() - if self.suppressComposite is not None: - not_composite = self.suppressComposite + # override the renderer default if self.suppressComposite + # is not None + not_composite = renderer.option_image_nocomposite() + if self.suppressComposite is not None: + not_composite = self.suppressComposite - if (len(self.images) <= 1 or not_composite or - not cbook.allequal([im.origin for im in self.images])): - for a in self.images: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) - else: - # make a composite image blending alpha - # list of (_image.Image, ox, oy) - mag = renderer.get_image_magnification() - ims = [(im.make_image(mag), im.ox, im.oy, im.get_alpha()) - for im in self.images] - - im = _image.from_images(int(self.bbox.height * mag), - int(self.bbox.width * mag), - ims) - - im.is_grayscale = False - l, b, w, h = self.bbox.bounds - - def draw_composite(): - gc = renderer.new_gc() - gc.set_clip_rectangle(self.bbox) - gc.set_clip_path(self.get_clip_path()) - renderer.draw_image(gc, l, b, im) - gc.restore() - - dsu.append((self.images[0].get_zorder(), self.images[0], - draw_composite, [])) - - # render the axes - for a in self.axes: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) - - # render the figure text - for a in self.texts: + if (len(self.images) <= 1 or not_composite or + not cbook.allequal([im.origin for im in self.images])): + for a in self.images: dsu.append((a.get_zorder(), a, a.draw, [renderer])) - - for a in self.legends: - dsu.append((a.get_zorder(), a, a.draw, [renderer])) - - dsu = [row for row in dsu if not row[1].get_animated()] - dsu.sort(key=itemgetter(0)) - for zorder, a, func, args in dsu: - func(*args) - finally: - renderer.close_group('figure') - self.stale = False + else: + # make a composite image blending alpha + # list of (_image.Image, ox, oy) + mag = renderer.get_image_magnification() + ims = [(im.make_image(mag), im.ox, im.oy, im.get_alpha()) + for im in self.images] + + im = _image.from_images(int(self.bbox.height * mag), + int(self.bbox.width * mag), + ims) + + im.is_grayscale = False + l, b, w, h = self.bbox.bounds + + def draw_composite(): + gc = renderer.new_gc() + gc.set_clip_rectangle(self.bbox) + gc.set_clip_path(self.get_clip_path()) + renderer.draw_image(gc, l, b, im) + gc.restore() + + dsu.append((self.images[0].get_zorder(), self.images[0], + draw_composite, [])) + + # render the axes + for a in self.axes: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) + + # render the figure text + for a in self.texts: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) + + for a in self.legends: + dsu.append((a.get_zorder(), a, a.draw, [renderer])) + + dsu = [row for row in dsu if not row[1].get_animated()] + dsu.sort(key=itemgetter(0)) + for zorder, a, func, args in dsu: + func(*args) + + renderer.close_group('figure') + self.stale = False self._cachedRenderer = renderer self.canvas.draw_event(renderer) From 0af89d747c7876f40dc64ba4046e29edee026721 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 01:00:28 -0400 Subject: [PATCH 16/30] FIX: fix stupid bug in pyplot --- lib/matplotlib/pyplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2cb872d44055..0d13caf64f19 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -535,7 +535,7 @@ def make_active(event): if _INSTALL_FIG_OBSERVER: fig.stale_callback = _auto_draw_if_interactive - return fig + return figManager.canvas.figure def _auto_draw_if_interactive(fig, val): From 14b5a36ad1ec1a6e1a75c0e9defd428544708128 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 1 Aug 2015 01:20:08 -0400 Subject: [PATCH 17/30] FIX: don't push race condition on un-pickling --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 4fc76bcfbafb..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): """ From 252d7a7b80a4058cb2412b6b220e1007d2a62b2d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Aug 2015 17:26:16 -0400 Subject: [PATCH 18/30] MNT: make base draw_idle re-entrant Calling `draw_idle` while executing a `draw_idle` will no-longer infinitely recurse. --- lib/matplotlib/backend_bases.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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): """ From a44b74dec8f05dbbc09bad32e1f8b5b9d452f9af Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Aug 2015 22:22:17 -0400 Subject: [PATCH 19/30] FIX: fix documentation typos --- examples/pylab_examples/system_monitor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/pylab_examples/system_monitor.py b/examples/pylab_examples/system_monitor.py index 964e4bcedb66..4d8c2dac24c5 100644 --- a/examples/pylab_examples/system_monitor.py +++ b/examples/pylab_examples/system_monitor.py @@ -52,11 +52,11 @@ def get_stats(): # 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 frame works event loop. + # of the GUI frameworks event loop. fig.canvas.draw_idle() try: - # make sure that the GUI framework has a chance to run it's event loop - # and clear and GUI events. This needs to be in a try/except block + # 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() From 53e2db2c4e22a21247ed6285ba129d2082f73a7f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Aug 2015 22:35:07 -0400 Subject: [PATCH 20/30] MNT: use no-op callback instead of None Elimnate need for `is None` checks --- lib/matplotlib/artist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 396e8dc4692a..b9cd1d81575b 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -83,7 +83,7 @@ class Artist(object): def __init__(self): self._stale = True - self.stale_callback = None + self.stale_callback = lambda self, value: None self._axes = None self.figure = None @@ -241,7 +241,7 @@ def stale(self, val): if self.get_animated(): return - if val and self.stale_callback is not None: + if val: self.stale_callback(self, val) def get_window_extent(self, renderer): From 54e04d353f823c6c9c1c4c7ec71db182f68fd300 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 9 Aug 2015 00:24:36 -0400 Subject: [PATCH 21/30] FIX: remove stale callback on pickling --- lib/matplotlib/artist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index b9cd1d81575b..002ca2ab4838 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -121,6 +121,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): From 5bd4711495fd0c38eca1fce9f91db3ba0b3f603f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 10 Aug 2015 01:07:08 -0400 Subject: [PATCH 22/30] MNT: make figure use base-class __getstate__ This make sure that the artist-level unpickable attributes are picked off. --- lib/matplotlib/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 92c95d654e9c..7edd92f76497 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1360,7 +1360,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, From a48be9f88538e7f90f3e544d2ff8ec0077051103 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 10 Aug 2015 01:07:47 -0400 Subject: [PATCH 23/30] MNT: defaulting `Artist.stale_callback` to None This reverts 53e2db2c4e22a21247ed6285ba129d2082f73a7f to make pickle/unpickle easier to deal with. This could also be dealt with by making sure that `__setstate__` restores the no-op, but that would require touching a lot more code. --- lib/matplotlib/artist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 002ca2ab4838..393627d2e61a 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -83,7 +83,7 @@ class Artist(object): def __init__(self): self._stale = True - self.stale_callback = lambda self, value: None + self.stale_callback = None self._axes = None self.figure = None @@ -242,7 +242,7 @@ def stale(self, val): if self.get_animated(): return - if val: + if val and self.stale_callback is not None: self.stale_callback(self, val) def get_window_extent(self, renderer): From 13e7c0976b6ec96967954b62960d97b81af1b754 Mon Sep 17 00:00:00 2001 From: James Evans Date: Tue, 11 Aug 2015 08:25:33 -0700 Subject: [PATCH 24/30] FIX: stale propagation in artist without fig/ax Artists do not properly guard against being removed from figures or Axes when tracking 'stale'. This will keep them from throwing an exception if they become stale after being removed from an Axes or Figure. --- lib/matplotlib/artist.py | 5 +++-- lib/matplotlib/figure.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 393627d2e61a..ec287d787934 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -68,8 +68,9 @@ def draw_wrapper(artist, renderer, *args, **kwargs): return draw_wrapper -def _stale_axes_callback(self, value): - self.axes.stale = True +def _stale_axes_callback(self, val): + if self.axes: + self.axes.stale = val class Artist(object): diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7edd92f76497..4256a6d04503 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -52,7 +52,8 @@ def _stale_figure_callback(self, val): - self.figure.stale = val + if self.figure: + self.figure.stale = val class AxesStack(Stack): From 83ea0206b97eb6217d04286a28188ce14fdf6e5d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:18:46 -0400 Subject: [PATCH 25/30] MNT: make sorting out what to re-draw faster Look at figures, not axes, and use a set --- lib/matplotlib/animation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 99b6c4d8d23d..69d1e35dc850 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(True) # 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): ''' From a5f5ab51adfe64351879ab1fe0ef87a9e8dbf165 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:20:50 -0400 Subject: [PATCH 26/30] MNT: make set_animated smarter - short circuit if not really chaning state - don't fire stale trigger on changing animation state --- lib/matplotlib/artist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index ec287d787934..010bcfc1daf3 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -814,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): """ From c75f75dc4f0562c0172687a9e2d4dcb62c74a152 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:21:26 -0400 Subject: [PATCH 27/30] MNT: when blitting in make artist animated When using blitting in animation make sure to set the blitted artsts as animated so that changing their state does not trigger the normal stale -> draw_idle cascade. --- lib/matplotlib/animation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 69d1e35dc850..06a8be78c5fd 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1094,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(True) def _draw_frame(self, framedata): # Save the data for potential saving of movies. @@ -1106,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(True) From 51cc0ca5b5c9e5d9a021b3a3733c9625ba53943b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:44:56 -0400 Subject: [PATCH 28/30] MNT: set animated to blit state --- lib/matplotlib/animation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 06a8be78c5fd..f4ec996f469c 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -975,7 +975,7 @@ def _init_draw(self): for f in self.new_frame_seq(): for artist in f: artist.set_visible(False) - artist.set_animated(True) + artist.set_animated(self._blit) # Assemble a list of unique axes that need flushing if artist.axes.figure not in figs: figs.add(artist.axes.figure) @@ -1095,7 +1095,7 @@ def _init_draw(self): else: self._drawn_artists = self._init_func() for a in self._drawn_artists: - a.set_animated(True) + a.set_animated(self._blit) def _draw_frame(self, framedata): # Save the data for potential saving of movies. @@ -1109,4 +1109,4 @@ def _draw_frame(self, framedata): # 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(True) + a.set_animated(self._blit) From 0e201f9806beb2603fe0547d94ce9920d033c645 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:50:32 -0400 Subject: [PATCH 29/30] MNT: setting cmap/norm marks as stale --- lib/matplotlib/image.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 01e36e831785..34a1dd9d3a83 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): From 25079bf8f453e766ebc43a6e1b3c282ad7db67d9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 12 Aug 2015 00:50:45 -0400 Subject: [PATCH 30/30] MNT: set_data on FigureImage marks as stale --- lib/matplotlib/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 34a1dd9d3a83..bd5d459193d4 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1034,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."""