Skip to content

WIP : major Axes refactor #3944

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 8 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
7 changes: 7 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ def remove(self):
# protected attribute if Python supported that sort of thing. The
# callback has one parameter, which is the child to be removed.
if self._remove_method is not None:
# set the current axes to None
self._axes = None
# use the call back registered by the axes when the artist
# was added to remove it the artist from the axes
self._remove_method(self)
else:
raise NotImplementedError('cannot remove artist')
Expand Down Expand Up @@ -701,6 +705,9 @@ def get_rasterized(self):
"return True if the artist is to be rasterized"
return self._rasterized

def set_remove_method(self, f):
self._remove_method = f

def set_rasterized(self, rasterized):
"""
Force rasterized (bitmap) drawing in vector backend output.
Expand Down
28 changes: 18 additions & 10 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import six
from six.moves import reduce, xrange, zip, zip_longest

import itertools
import math
import warnings

Expand All @@ -13,7 +13,7 @@
import matplotlib

import matplotlib.cbook as cbook
from matplotlib.cbook import _string_to_bool, mplDeprecation
from matplotlib.cbook import mplDeprecation
import matplotlib.collections as mcoll
import matplotlib.colors as mcolors
import matplotlib.contour as mcontour
Expand All @@ -35,7 +35,8 @@
import matplotlib.transforms as mtransforms
import matplotlib.tri as mtri
import matplotlib.transforms as mtrans
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
from matplotlib.container import (BarContainer, ErrorbarContainer,
StemContainer, Container)
from matplotlib.axes._base import _AxesBase
from matplotlib.axes._base import _process_plot_format

Expand Down Expand Up @@ -2666,6 +2667,8 @@ def errorbar(self, x, y, yerr=None, xerr=None,

label = kwargs.pop("label", None)

zorder = kwargs.pop('zorder', 0)

# make sure all the args are iterable; use lists not arrays to
# preserve units
if not iterable(x):
Expand Down Expand Up @@ -2755,7 +2758,7 @@ def xywhere(xs, ys, mask):

if xerr is not None:
if (iterable(xerr) and len(xerr) == 2 and
iterable(xerr[0]) and iterable(xerr[1])):
iterable(xerr[0]) and iterable(xerr[1])):
# using list comps rather than arrays to preserve units
left = [thisx - thiserr for (thisx, thiserr)
in cbook.safezip(x, xerr[0])]
Expand Down Expand Up @@ -2805,7 +2808,7 @@ def xywhere(xs, ys, mask):
else:
marker = mlines.CARETLEFT
caplines.extend(
self.plot(leftlo, ylo, ls='None', marker=marker,
self.plot(leftlo, ylo, ls='None', marker=marker,
**plot_kw))
if capsize > 0:
xup, yup = xywhere(x, y, xuplims & everymask)
Expand Down Expand Up @@ -2886,14 +2889,19 @@ def xywhere(xs, ys, mask):
self.autoscale_view()
self._hold = holdstate

errorbar_container = ErrorbarContainer((l0, tuple(caplines),
tuple(barcols)),
# hack to put these artist in the right place in the
# draw tree
for ll in itertools.chain((l0, ), caplines, barcols):
ll.remove()

errorbar_container = ErrorbarContainer((l0,
Container(caplines),
Container(barcols)),
has_xerr=(xerr is not None),
has_yerr=(yerr is not None),
label=label)
self.containers.append(errorbar_container)

return errorbar_container # (l0, caplines, barcols)
errorbar_container.set_zorder(zorder)
return self.add_container(errorbar_container)

def boxplot(self, x, notch=False, sym=None, vert=True, whis=1.5,
positions=None, widths=None, patch_artist=False,
Expand Down
13 changes: 10 additions & 3 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1605,13 +1605,19 @@ def add_container(self, container):
Add a :class:`~matplotlib.container.Container` instance
to the axes.

Returns the collection.
Returns the container.
"""
container.set_axes(self)
self._set_artist_props(container)
self.containers.append(container)

container.set_clip_path(self.patch)
container.set_remove_method(lambda h: self.containers.remove(h))

label = container.get_label()
if not label:
container.set_label('_container%d' % len(self.containers))
self.containers.append(container)
container.set_remove_method(lambda h: self.containers.remove(h))

return container

def relim(self, visible_only=False):
Expand Down Expand Up @@ -1998,6 +2004,7 @@ def draw(self, renderer=None, inframe=False):
artists.extend(self.lines)
artists.extend(self.texts)
artists.extend(self.artists)
artists.extend(self.containers)

# the frame draws the edges around the axes patch -- we
# decouple these so the patch can be in the background and the
Expand Down
64 changes: 64 additions & 0 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2311,6 +2311,70 @@ def get_instancemethod(self):
return getattr(self.parent_obj, self.instancemethod_name)


_art_list_msg = ("The use of Axes.lines, Axes.patches, Axes.texts "
"Axes.tables, Axes.artists, Axes.collections, "
"Axes.containers, "
"and Axes.images have been "
"deprecated. All artists are now stored is a single "
"tree accessible via the Axes.artist_tree property. "
"Please use the ``remove`` method on the artists to remove"
"them from an Axes. \n\n"
"These lists will be removed in 1.7 or 2.0.")


class MPLRemoverList(list):
"""

This is a sub-class of list which implements the logic to manage the
backwards compatibility during deprecation of the lines, patches,
texts, tables, artists, images, collections, and containers
attributes from the Axes class. This will allow users to continue
to use `ax.lines.pop()` to remove lines from an axes, even though
the draw method no longer looks at those lists at render time.

This class will be removed when the list are.

"""
def __delslice__(self, a, b):
# warn
warnings.warn(_art_list_msg, mplDeprecation, stacklevel=1)
# grab what we will be removing
res = self[a:b]
# remove it from this list
super(MPLRemoverList, self).__delslice__(self, a, b)
# see if we need to call the real remove
# Artist.remove sets _axes = None so if this is called
# remove it won't be called again, but if a user removes
# an artist from these lists directly, remove will correctly
# be called.
for a in res:
# this works because of details of how Artist.remove works
if a.axes:
a.remove()

def __delitem__(self, y):
# see __delslice__ for explanation of logic
warnings.warn(_art_list_msg, mplDeprecation, stacklevel=1)
res = self[y]
super(MPLRemoverList, self).__delitem__(self, y)
if res.axes:
res.remove()

def pop(self, i):
# see __delslice__ for explanation of logic
warnings.warn(_art_list_msg, mplDeprecation, stacklevel=1)
res = super(MPLRemoverList, self).pop(self, i)
if res.axes:
res.remove()

def remove(self, item):
# see __delslice__ for explanation of logic
warnings.warn(_art_list_msg, mplDeprecation, stacklevel=1)
res = super(MPLRemoverList, self).remove(self, item)
if item.axes:
res.remove()


# Numpy > 1.6.x deprecates putmask in favor of the new copyto.
# So long as we support versions 1.6.x and less, we need the
# following local version of putmask. We choose to make a
Expand Down
118 changes: 41 additions & 77 deletions lib/matplotlib/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,107 +2,71 @@
unicode_literals)

import six

from matplotlib.artist import Artist, allow_rasterization
import matplotlib.cbook as cbook


class Container(tuple):
class Container(tuple, Artist):
"""
Base class for containers.
"""
_no_broadcast = ['label', 'visible', 'zorder', 'animated',
'agg_filter']

def __repr__(self):
return "<Container object of %d artists>" % (len(self))

def __new__(cls, *kl, **kwargs):
return tuple.__new__(cls, kl[0])

def __init__(self, kl, label=None):

self.eventson = False # fire events only if eventson
self._oid = 0 # an observer id
self._propobservers = {} # a dict from oids to funcs

self._remove_method = None

def __init__(self, kl, label=None, **kwargs):
# set up the artist details
Artist.__init__(self, **kwargs)
# for some reason we special case label
self.set_label(label)

def set_remove_method(self, f):
self._remove_method = f

def remove(self):
# remove the children
for c in self:
c.remove()

if self._remove_method:
self._remove_method(self)

def __getstate__(self):
d = self.__dict__.copy()
# 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
return d

def get_label(self):
"""
Get the label used for this artist in the legend.
"""
return self._label

def set_label(self, s):
"""
Set the label to *s* for auto legend.

ACCEPTS: string or anything printable with '%s' conversion.
"""
if s is not None:
self._label = '%s' % (s, )
else:
self._label = None
self.pchanged()

def add_callback(self, func):
"""
Adds a callback function that will be called whenever one of
the :class:`Artist`'s properties changes.

Returns an *id* that is useful for removing the callback with
:meth:`remove_callback` later.
"""
oid = self._oid
self._propobservers[oid] = func
self._oid += 1
return oid

def remove_callback(self, oid):
"""
Remove a callback based on its *id*.

.. seealso::

:meth:`add_callback`
For adding callbacks

"""
try:
del self._propobservers[oid]
except KeyError:
pass

def pchanged(self):
"""
Fire an event when property changed, calling all of the
registered callbacks.
"""
for oid, func in list(six.iteritems(self._propobservers)):
func(self)
# call up to the Artist remove method
super(Container, self).remove(self)

def get_children(self):
return list(cbook.flatten(self))

def __getattribute__(self, key):

# broadcast set_* and get_* methods across members
# except for these explicitly not.
if (('set' in key or 'get' in key) and
all(k not in key for k in self._no_broadcast)):

def inner(*args, **kwargs):
return [getattr(a, key)(*args, **kwargs)
for a in self]
inner.__name__ = key
doc = getattr(self[0], key).__doc__
inner.__doc__ = doc
return inner
else:
return super(Container, self).__getattribute__(key)

@allow_rasterization
def draw(self, renderer, *args, **kwargs):
# just broadcast the draw down to children
for a in self:
a.draw(renderer, *args, **kwargs)


class BarContainer(Container):
def __new__(cls, patches, errorbar=None, **kwargs):
if errorbar is None:
errorbar = tuple()
else:
errorbar = tuple(errorbar)
patches = tuple(patches)
return super(BarContainer, cls).__new__(patches + errorbar, **kwargs)

def __init__(self, patches, errorbar=None, **kwargs):
self.patches = patches
Expand Down