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 986870c78140..17315b2de19a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -25,6 +25,9 @@ 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 @@ -233,7 +236,6 @@ def _update_this(self, s, val): setattr(self, s, val) - class Figure(Artist): """ @@ -346,11 +348,44 @@ 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 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.canvas.draw() + # 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: 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 _repr_html_(self): # We can't use "isinstance" here, because then we'd end up importing # webagg unconditiionally. @@ -495,6 +530,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. @@ -520,6 +556,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 +578,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): @@ -1211,6 +1249,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): """ @@ -1228,7 +1267,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 +1275,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): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 029ee9aeda15..7f12942b44ab 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 @@ -2763,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) @@ -3097,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)