diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7fb492367f1d..980958fd12c3 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -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') @@ -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. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index fb82fd59b256..76335f7ea75b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3,7 +3,7 @@ import six from six.moves import reduce, xrange, zip, zip_longest - +import itertools import math import warnings @@ -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 @@ -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 @@ -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): @@ -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])] @@ -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) @@ -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, diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index e7b9696947c9..fa2b9b86aa00 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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): @@ -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 diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c0f66c9f27cd..22e8974a5d2c 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -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 diff --git a/lib/matplotlib/container.py b/lib/matplotlib/container.py index 4603e7e7bee0..8305798d1eba 100644 --- a/lib/matplotlib/container.py +++ b/lib/matplotlib/container.py @@ -2,14 +2,16 @@ 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 "" % (len(self)) @@ -17,92 +19,54 @@ def __repr__(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