Skip to content

Deprecate axes collision #9037

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

Merged
merged 2 commits into from
Aug 15, 2017
Merged
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
148 changes: 65 additions & 83 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ class AxesStack(Stack):

"""
def __init__(self):
cbook.warn_deprecated("2.1")
Stack.__init__(self)
self._ind = 0

Expand All @@ -92,6 +91,13 @@ def get(self, key):
item = dict(self._elements).get(key)
if item is None:
return None
cbook.warn_deprecated(
"2.1",
"Adding an axes using the same arguments as a previous axes "
"currently reuses the earlier instance. In a future version, "
"a new instance will always be created and returned. Meanwhile, "
"this warning can be suppressed, and the future behavior ensured, "
"by passing a unique label to each axes instance.")
return item[1]

def _entry_from_axes(self, e):
Expand Down Expand Up @@ -159,62 +165,6 @@ def __contains__(self, a):
return a in self.as_list()


class _AxesStack(object):
"""Lightweight stack that tracks Axes in a Figure.
"""

def __init__(self):
# We maintain a list of (creation_index, key, axes) tuples.
# We do not use an OrderedDict because 1. the keys may not be hashable
# and 2. we need to directly find a pair corresponding to an axes (i.e.
# we'd really need a two-way dict).
self._items = []
self._created = 0

def as_list(self):
"""Copy of the list of axes, in the order of insertion.
"""
return [ax for _, _, ax in sorted(self._items)]

def get(self, key):
"""Find the axes corresponding to a key; defaults to `None`.
"""
return next((ax for _, k, ax in self._items if k == key), None)

def current_key_axes(self):
"""Return the topmost `(key, axes)` pair, or `(None, None)` if empty.
"""
_, key, ax = (self._items or [(None, None, None)])[-1]
return key, ax

def add(self, key, ax):
"""Append a `(key, axes)` pair, unless the axes are already present.
"""
# Skipping existing Axes is needed to support calling `add_axes` with
# an already existing Axes.
if not any(a == ax for _, _, a in self._items):
self._items.append((self._created, key, ax))
self._created += 1

def bubble(self, ax):
"""Move an axes and its corresponding key to the top.
"""
idx, = (idx for idx, (_, _, a) in enumerate(self._items) if a == ax)
self._items.append(self._items[idx])
del self._items[idx]

def remove(self, ax):
"""Remove an axes and its corresponding key.
"""
idx, = (idx for idx, (_, _, a) in enumerate(self._items) if a == ax)
del self._items[idx]

def clear(self):
"""Clear the stack.
"""
del self._items[:]


class SubplotParams(object):
"""
A class to hold the parameters for a subplot
Expand Down Expand Up @@ -415,7 +365,7 @@ def __init__(self,
self.subplotpars = subplotpars
self.set_tight_layout(tight_layout)

self._axstack = _AxesStack() # track all figure axes and current axes
self._axstack = AxesStack() # track all figure axes and current axes
self.clf()
self._cachedRenderer = None

Expand Down Expand Up @@ -467,8 +417,10 @@ def show(self, warn=True):
"matplotlib is currently using a non-GUI backend, "
"so cannot show the figure")

axes = property(lambda self: self._axstack.as_list(),
doc="Read-only: list of axes in Figure")
def _get_axes(self):
return self._axstack.as_list()

axes = property(fget=_get_axes, doc="Read-only: list of axes in Figure")

def _get_dpi(self):
return self._dpi
Expand Down Expand Up @@ -890,6 +842,36 @@ def delaxes(self, a):
func(self)
self.stale = True

def _make_key(self, *args, **kwargs):
'make a hashable key out of args and kwargs'

def fixitems(items):
#items may have arrays and lists in them, so convert them
# to tuples for the key
ret = []
for k, v in items:
# some objects can define __getitem__ without being
# iterable and in those cases the conversion to tuples
# will fail. So instead of using the iterable(v) function
# we simply try and convert to a tuple, and proceed if not.
try:
v = tuple(v)
except Exception:
pass
ret.append((k, v))
return tuple(ret)

def fixlist(args):
ret = []
for a in args:
if iterable(a):
a = tuple(a)
ret.append(a)
return tuple(ret)

key = fixlist(args), fixitems(six.iteritems(kwargs))
return key

def add_axes(self, *args, **kwargs):
"""
Add an axes at position *rect* [*left*, *bottom*, *width*,
Expand Down Expand Up @@ -926,14 +908,14 @@ def add_axes(self, *args, **kwargs):
fig.add_axes(rect, projection='polar')
fig.add_axes(ax)

If the figure already has an axes with the same parameters,
then it will simply make that axes current and return it. If
you do not want this behavior, e.g., you want to force the
creation of a new Axes, you must use a unique set of args and
kwargs. The axes :attr:`~matplotlib.axes.Axes.label`
attribute has been exposed for this purpose. e.g., if you want
two axes that are otherwise identical to be added to the
figure, make sure you give them unique labels::
If the figure already has an axes with the same parameters, then it
will simply make that axes current and return it. This behavior
has been deprecated as of Matplotlib 2.1. Meanwhile, if you do
not want this behavior (i.e., you want to force the creation of a
new Axes), you must use a unique set of args and kwargs. The axes
:attr:`~matplotlib.axes.Axes.label` attribute has been exposed for this
purpose: if you want two axes that are otherwise identical to be added
to the figure, make sure you give them unique labels::

fig.add_axes(rect, label='axes1')
fig.add_axes(rect, label='axes2')
Expand All @@ -954,9 +936,9 @@ def add_axes(self, *args, **kwargs):

# shortcut the projection "key" modifications later on, if an axes
# with the exact args/kwargs exists, return it immediately.
key = (args, kwargs)
key = self._make_key(*args, **kwargs)
ax = self._axstack.get(key)
if ax:
if ax is not None:
self.sca(ax)
return ax

Expand All @@ -976,7 +958,7 @@ def add_axes(self, *args, **kwargs):
# check that an axes of this type doesn't already exist, if it
# does, set it as active and return it
ax = self._axstack.get(key)
if isinstance(ax, projection_class):
if ax is not None and isinstance(ax, projection_class):
self.sca(ax)
return ax

Expand Down Expand Up @@ -1021,14 +1003,14 @@ def add_subplot(self, *args, **kwargs):
-----
If the figure already has a subplot with key (*args*,
*kwargs*) then it will simply make that subplot current and
return it.
return it. This behavior is deprecated.

Examples
--------
fig.add_subplot(111)

# equivalent but more general
fig.add_subplot(1,1,1)
fig.add_subplot(1, 1, 1)

# add subplot with red background
fig.add_subplot(212, facecolor='r')
Expand All @@ -1047,29 +1029,29 @@ def add_subplot(self, *args, **kwargs):
return

if len(args) == 1 and isinstance(args[0], int):
args = tuple([int(c) for c in str(args[0])])
if len(args) != 3:
raise ValueError("Integer subplot specification must " +
"be a three digit number. " +
"Not {n:d}".format(n=len(args)))
if not 100 <= args[0] <= 999:
raise ValueError("Integer subplot specification must be a "
"three-digit number, not {}".format(args[0]))
args = tuple(map(int, str(args[0])))

if isinstance(args[0], SubplotBase):

a = args[0]
if a.get_figure() is not self:
msg = ("The Subplot must have been created in the present"
" figure")
msg = ("The Subplot must have been created in the present "
"figure")
raise ValueError(msg)
# make a key for the subplot (which includes the axes object id
# in the hash)
key = (args, kwargs)
key = self._make_key(*args, **kwargs)
else:
projection_class, kwargs, key = process_projection_requirements(
self, *args, **kwargs)

# try to find the axes with this key in the stack
ax = self._axstack.get(key)
if ax:

if ax is not None:
if isinstance(ax, projection_class):
# the axes already existed, so set it as active & return
self.sca(ax)
Expand Down Expand Up @@ -1638,7 +1620,7 @@ def _gci(self):
do not use elsewhere.
"""
# Look first for an image in the current Axes:
ckey, cax = self._axstack.current_key_axes()
cax = self._axstack.current_key_axes()[1]
if cax is None:
return None
im = cax._gci()
Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/projections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ def process_projection_requirements(figure, *args, **kwargs):
raise TypeError('projection must be a string, None or implement a '
'_as_mpl_axes method. Got %r' % projection)

return projection_class, kwargs, (args, kwargs)
# Make the key without projection kwargs, this is used as a unique
# lookup for axes instances
key = figure._make_key(*args, **kwargs)

return projection_class, kwargs, key


def get_projection_names():
Expand Down