From 0799c2e71e49db47d58b680973666695b255678c Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sun, 28 Sep 2014 20:47:39 -1000 Subject: [PATCH 1/4] interactive OO: simple test with Figure.suptitle and Figure.text --- lib/matplotlib/figure.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 986870c78140..4bccc7f30d28 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -24,6 +24,7 @@ from matplotlib import rcParams from matplotlib import docstring from matplotlib import __version__ as _mpl_version +from matplotlib import is_interactive import matplotlib.artist as martist from matplotlib.artist import Artist, allow_rasterization @@ -346,11 +347,39 @@ def __init__(self, self._axstack = AxesStack() # track all figure axes and current axes self.clf() self._cachedRenderer = None + self._in_outer_method = False # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't # use it, for some reason. + def draw_if_interactive(self, outer=False): + # Not sure whether this should be public or private... + if outer or not is_interactive(): + return + # Leave out the following check for now; it probably + # has to be modified so that it does not require importing + # all the available interactive backends just to make + # the list of canvases. Instead, the check could be based + # on the str() or (repr) of self.canvas. + #if not isinstance(self.canvas, interactive_canvases): + # return + self.canvas.draw() + print("drawing complete") + self._in_outer_method = False + + def check_interactive(self): + if not self._in_outer_method: + self._in_outer_method = True + print("checking: True") + return True + print("checking: False") + return False + + def clear_interactive(self): + self._in_outer_method = False + + def _repr_html_(self): # We can't use "isinstance" here, because then we'd end up importing # webagg unconditiionally. @@ -520,6 +549,7 @@ def suptitle(self, t, **kwargs): fig.suptitle('this is the figure title', fontsize=12) """ + outer = self.check_interactive() x = kwargs.pop('x', 0.5) y = kwargs.pop('y', 0.98) @@ -541,6 +571,7 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup + self.draw_if_interactive(outer) return self._suptitle def set_canvas(self, canvas): @@ -1228,7 +1259,7 @@ def text(self, x, y, s, *args, **kwargs): %(Text)s """ - + outer = self.check_interactive() override = _process_text_args({}, *args, **kwargs) t = Text(x=x, y=y, text=s) @@ -1236,6 +1267,7 @@ def text(self, x, y, s, *args, **kwargs): self._set_artist_props(t) self.texts.append(t) t._remove_method = lambda h: self.texts.remove(h) + self.draw_if_interactive(outer) return t def _set_artist_props(self, a): From 4a4948d891e69cbd17827c50ac7e7cd5bb8052b3 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 18 Oct 2014 17:33:07 -1000 Subject: [PATCH 2/4] checkpoint commit: using decorator --- lib/matplotlib/figure.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 4bccc7f30d28..2c546d1f8899 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -234,6 +234,19 @@ def _update_this(self, s, val): setattr(self, s, val) +import functools +def _interactive(func): + @functools.wraps(func) + def inner(self, *args, **kw): + outer = self.check_interactive() + drawn = True # default in "finally" clause is to clear. + try: + ret = func(self, *args, **kw) + drawn = self.draw_if_interactive(outer) + finally: + self.clear_interactive(drawn) + return ret + return inner class Figure(Artist): @@ -355,8 +368,8 @@ def __init__(self, def draw_if_interactive(self, outer=False): # Not sure whether this should be public or private... - if outer or not is_interactive(): - return + if not outer or not is_interactive(): + return False # Leave out the following check for now; it probably # has to be modified so that it does not require importing # all the available interactive backends just to make @@ -366,7 +379,8 @@ def draw_if_interactive(self, outer=False): # return self.canvas.draw() print("drawing complete") - self._in_outer_method = False + #self._in_outer_method = False + return True def check_interactive(self): if not self._in_outer_method: @@ -376,8 +390,9 @@ def check_interactive(self): print("checking: False") return False - def clear_interactive(self): - self._in_outer_method = False + def clear_interactive(self, drawn): + if drawn: + self._in_outer_method = False def _repr_html_(self): @@ -524,6 +539,7 @@ def get_window_extent(self, *args, **kwargs): 'get the figure bounding box in display space; kwargs are void' return self.bbox + @_interactive def suptitle(self, t, **kwargs): """ Add a centered title to the figure. @@ -549,7 +565,7 @@ def suptitle(self, t, **kwargs): fig.suptitle('this is the figure title', fontsize=12) """ - outer = self.check_interactive() + #outer = self.check_interactive() x = kwargs.pop('x', 0.5) y = kwargs.pop('y', 0.98) @@ -571,7 +587,7 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup - self.draw_if_interactive(outer) + #self.draw_if_interactive(outer) return self._suptitle def set_canvas(self, canvas): @@ -1242,6 +1258,7 @@ def legend(self, handles, labels, *args, **kwargs): l._remove_method = lambda h: self.legends.remove(h) return l + @_interactive @docstring.dedent_interpd def text(self, x, y, s, *args, **kwargs): """ @@ -1259,7 +1276,7 @@ def text(self, x, y, s, *args, **kwargs): %(Text)s """ - outer = self.check_interactive() + #outer = self.check_interactive() override = _process_text_args({}, *args, **kwargs) t = Text(x=x, y=y, text=s) @@ -1267,7 +1284,7 @@ def text(self, x, y, s, *args, **kwargs): self._set_artist_props(t) self.texts.append(t) t._remove_method = lambda h: self.texts.remove(h) - self.draw_if_interactive(outer) + #self.draw_if_interactive(outer) return t def _set_artist_props(self, a): From 8838673c6a8cd91e5e5854c15838079e4ac5a8d8 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sun, 23 Nov 2014 15:18:12 -0500 Subject: [PATCH 3/4] remove draw_if_interactive lines that are no longer needed --- lib/matplotlib/figure.py | 11 +++++++---- lib/matplotlib/pyplot.py | 2 -- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 2c546d1f8899..a864d997500d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -378,16 +378,19 @@ def draw_if_interactive(self, outer=False): #if not isinstance(self.canvas, interactive_canvases): # return self.canvas.draw() - print("drawing complete") - #self._in_outer_method = False + # print("drawing complete") return True def check_interactive(self): + """ + Return True upon entering an outer method, and set the + flag; return False if already in an outer method. + """ if not self._in_outer_method: self._in_outer_method = True - print("checking: True") + print("checking: toggled _in_outer_method to True") return True - print("checking: False") + print("checking: already _in_outer_method; returning False") return False def clear_interactive(self, drawn): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 029ee9aeda15..217ef06fd922 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -611,14 +611,12 @@ def waitforbuttonpress(*args, **kwargs): def figtext(*args, **kwargs): ret = gcf().text(*args, **kwargs) - draw_if_interactive() return ret @docstring.copy_dedent(Figure.suptitle) def suptitle(*args, **kwargs): ret = gcf().suptitle(*args, **kwargs) - draw_if_interactive() return ret From 4350bbf9f7f37ae1b4e8b8c0daf48bee2f2537b6 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 7 Feb 2015 21:22:53 -1000 Subject: [PATCH 4/4] Extend the example to plt.plot and plt.errorbar --- lib/matplotlib/__init__.py | 25 ++++++++++++++++++++++++ lib/matplotlib/axes/_axes.py | 3 +++ lib/matplotlib/axes/_base.py | 38 ++++++++++++++++++++++++++++++++++-- lib/matplotlib/figure.py | 16 ++------------- lib/matplotlib/pyplot.py | 4 ++-- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index b829425d143d..abd1b1452ec7 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -201,6 +201,31 @@ def _forward_ilshift(self, other): 'numpy %s or later is required; you have %s' % ( __version__numpy__, numpy.__version__)) +import functools +def _interactive(func): + """ + Decorator for Figure and Axes methods that will be wrapped in pyplot. + + Methods so decorated no longer need a call to `draw_if_interactive` + in their pyplot wrappers, and are interactive when invoked as + methods with a gui backend in interactive mode. + + Figure and Axes must define the methods `check_interactive`, + `draw_if_interactive`, and `clear_interactive` used in this + decorator. + """ + @functools.wraps(func) + def inner(self, *args, **kw): + outer = self.check_interactive() + drawn = True # default in "finally" clause is to clear. + try: + ret = func(self, *args, **kw) + drawn = self.draw_if_interactive(outer) + finally: + self.clear_interactive(drawn) + return ret + return inner + def _is_writable_dir(p): """ diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a21893fc3a45..aaa45c05578f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -38,6 +38,7 @@ from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.axes._base import _AxesBase from matplotlib.axes._base import _process_plot_format +from matplotlib import _interactive # decorator rcParams = matplotlib.rcParams @@ -1235,6 +1236,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, return colls #### Basic plotting + @_interactive @docstring.dedent_interpd def plot(self, *args, **kwargs): """ @@ -2530,6 +2532,7 @@ def pie(self, x, explode=None, labels=None, colors=None, else: return slices, texts, autotexts + @_interactive @docstring.dedent_interpd def errorbar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=3, diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ca635275aa31..320a1627a71d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -30,9 +30,8 @@ import matplotlib.text as mtext import matplotlib.image as mimage from matplotlib.artist import allow_rasterization - - from matplotlib.cbook import iterable +from matplotlib import is_interactive rcParams = matplotlib.rcParams @@ -442,6 +441,7 @@ def __init__(self, fig, rect, self.set_cursor_props((1, 'k')) # set the cursor properties for axes self._cachedRenderer = None + self._in_outer_method = False self.set_navigate(True) self.set_navigate_mode(None) @@ -461,6 +461,40 @@ def __init__(self, fig, rect, self._ycid = self.yaxis.callbacks.connect('units finalize', self.relim) + + def draw_if_interactive(self, outer=False): + print("entering draw_if_interactive in axes/_base, outer = ", outer) + # Not sure whether this should be public or private... + if not outer or not is_interactive(): + return False + # Leave out the following check for now; it probably + # has to be modified so that it does not require importing + # all the available interactive backends just to make + # the list of canvases. Instead, the check could be based + # on the str() or (repr) of self.canvas. + #if not isinstance(self.canvas, interactive_canvases): + # return + self.figure.canvas.draw() + print("drawing complete in axes/_base") + return True + + def check_interactive(self): + """ + Return True upon entering an outer method, and set the + flag; return False if already in an outer method. + """ + if not self._in_outer_method: + self._in_outer_method = True + print("checking: toggled _in_outer_method to True") + return True + print("checking: already _in_outer_method; returning False") + return False + + def clear_interactive(self, drawn): + if drawn: + self._in_outer_method = False + + def __setstate__(self, state): self.__dict__ = state # put the _remove_method back on all artists contained within the axes diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index a864d997500d..17315b2de19a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -24,7 +24,9 @@ from matplotlib import rcParams from matplotlib import docstring from matplotlib import __version__ as _mpl_version + from matplotlib import is_interactive +from matplotlib import _interactive # decorator import matplotlib.artist as martist from matplotlib.artist import Artist, allow_rasterization @@ -234,20 +236,6 @@ def _update_this(self, s, val): setattr(self, s, val) -import functools -def _interactive(func): - @functools.wraps(func) - def inner(self, *args, **kw): - outer = self.check_interactive() - drawn = True # default in "finally" clause is to clear. - try: - ret = func(self, *args, **kw) - drawn = self.draw_if_interactive(outer) - finally: - self.clear_interactive(drawn) - return ret - return inner - class Figure(Artist): """ diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 217ef06fd922..7f12942b44ab 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2761,7 +2761,7 @@ def errorbar(x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, barsabove=barsabove, lolims=lolims, uplims=uplims, xlolims=xlolims, xuplims=xuplims, errorevery=errorevery, capthick=capthick, **kwargs) - draw_if_interactive() + #draw_if_interactive() finally: ax.hold(washold) @@ -3095,7 +3095,7 @@ def plot(*args, **kwargs): ax.hold(hold) try: ret = ax.plot(*args, **kwargs) - draw_if_interactive() + #draw_if_interactive() finally: ax.hold(washold)