Skip to content

WIP: Oo interactive; enable interactive mode with Axes and Figure methods #4082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 36 additions & 2 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
44 changes: 42 additions & 2 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -233,7 +236,6 @@ def _update_this(self, s, val):

setattr(self, s, val)


class Figure(Artist):

"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand All @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand All @@ -1228,14 +1267,15 @@ 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)

t.update(override)
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):
Expand Down
6 changes: 2 additions & 4 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down