diff --git a/doc/api/next_api_changes/behavior/18216-ES.rst b/doc/api/next_api_changes/behavior/18216-ES.rst new file mode 100644 index 000000000000..d6e6cae4b55d --- /dev/null +++ b/doc/api/next_api_changes/behavior/18216-ES.rst @@ -0,0 +1,30 @@ +.. _Behavioral API Changes 3.5 - Axes children combined: + +``Axes`` children are no longer separated by type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Formerly, `.axes.Axes` children were separated by `.Artist` type, into sublists +such as ``Axes.lines``. For methods that produced multiple elements (such as +`.Axes.errorbar`), though individual parts would have similar *zorder*, this +separation might cause them to be drawn at different times, causing +inconsistent results when overlapping other Artists. + +Now, the children are no longer separated by type, and the sublist properties +are generated dynamically when accessed. Consequently, Artists will now always +appear in the correct sublist; e.g., if `.axes.Axes.add_line` is called on a +`.Patch`, it will be appear in the ``Axes.patches`` sublist, _not_ +``Axes.lines``. The ``Axes.add_*`` methods will now warn if passed an +unexpected type. + +Modification of the following sublists is still accepted, but deprecated: + +* ``Axes.artists`` +* ``Axes.collections`` +* ``Axes.images`` +* ``Axes.lines`` +* ``Axes.patches`` +* ``Axes.tables`` +* ``Axes.texts`` + +To remove an Artist, use its `.Artist.remove` method. To add an Artist, use the +corresponding ``Axes.add_*`` method. diff --git a/doc/api/next_api_changes/deprecations/18216-ES.rst b/doc/api/next_api_changes/deprecations/18216-ES.rst new file mode 100644 index 000000000000..56fa58c65c39 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18216-ES.rst @@ -0,0 +1,22 @@ +Modification of ``Axes`` children sublists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See :ref:`Behavioral API Changes 3.5 - Axes children combined` for more +information; modification of the following sublists is deprecated: + +* ``Axes.artists`` +* ``Axes.collections`` +* ``Axes.images`` +* ``Axes.lines`` +* ``Axes.patches`` +* ``Axes.tables`` +* ``Axes.texts`` + +To remove an Artist, use its `.Artist.remove` method. To add an Artist, use the +corresponding ``Axes.add_*`` method. + +Passing incorrect types to ``Axes.add_*`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Axes.add_*`` methods will now warn if passed an unexpected type. See +their documentation for the types they expect. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e93a5c1eb138..4d89a3cb161d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -866,9 +866,9 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs): if line.get_clip_path() is None: line.set_clip_path(self.patch) if not line.get_label(): - line.set_label(f"_line{len(self.lines)}") - self.lines.append(line) - line._remove_method = self.lines.remove + line.set_label(f"_child{len(self._children)}") + self._children.append(line) + line._remove_method = self._children.remove self.update_datalim(datalim) self._request_autoscale_view() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 728d22cd7d08..593390fe864f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from collections.abc import MutableSequence from contextlib import ExitStack import functools import inspect @@ -11,22 +12,23 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook -from matplotlib.cbook import _OrderedSet, _check_1d, index_of -from matplotlib import docstring -import matplotlib.colors as mcolors -import matplotlib.lines as mlines -import matplotlib.patches as mpatches +from matplotlib import _api, cbook, docstring import matplotlib.artist as martist -import matplotlib.transforms as mtransforms -import matplotlib.ticker as mticker import matplotlib.axis as maxis -import matplotlib.spines as mspines +from matplotlib.cbook import _OrderedSet, _check_1d, index_of +import matplotlib.collections as mcoll +import matplotlib.colors as mcolors import matplotlib.font_manager as font_manager -import matplotlib.text as mtext import matplotlib.image as mimage +import matplotlib.lines as mlines +import matplotlib.patches as mpatches import matplotlib.path as mpath from matplotlib.rcsetup import cycler, validate_axisbelow +import matplotlib.spines as mspines +import matplotlib.table as mtable +import matplotlib.text as mtext +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms _log = logging.getLogger(__name__) @@ -984,7 +986,9 @@ def _update_transScale(self): self.transScale.set( mtransforms.blended_transform_factory( self.xaxis.get_transform(), self.yaxis.get_transform())) - for line in getattr(self, "lines", []): # Not set during init. + for line in getattr(self, "_children", []): # Not set during init. + if not isinstance(line, mlines.Line2D): + continue try: line._transformed_path.invalidate() except AttributeError: @@ -1228,18 +1232,12 @@ def cla(self): self._get_patches_for_fill = _process_plot_var_args(self, 'fill') self._gridOn = mpl.rcParams['axes.grid'] - self.lines = [] - self.patches = [] - self.texts = [] - self.tables = [] - self.artists = [] - self.images = [] + self._children = [] self._mouseover_set = _OrderedSet() self.child_axes = [] self._current_image = None # strictly for pyplot via _sci, _gci self._projection_init = None # strictly for pyplot.subplot self.legend_ = None - self.collections = [] # collection.Collection instances self.containers = [] self.grid(False) # Disable grid on init to use rcParameter @@ -1307,6 +1305,155 @@ def cla(self): self.stale = True + class ArtistList(MutableSequence): + """ + A sublist of Axes children based on their type. + + This exists solely to warn on modification. In the future, the + type-specific children sublists will be immutable tuples. + """ + def __init__(self, axes, prop_name, add_name, + valid_types=None, invalid_types=None): + """ + Parameters + ---------- + axes : .axes.Axes + The Axes from which this sublist will pull the children + Artists. + prop_name : str + The property name used to access this sublist from the Axes; + used to generate deprecation warnings. + add_name : str + The method name used to add Artists of this sublist's type to + the Axes; used to generate deprecation warnings. + valid_types : list of type, optional + A list of types that determine which children will be returned + by this sublist. If specified, then the Artists in the sublist + must be instances of any of these types. If unspecified, then + any type of Artist is valid (unless limited by + *invalid_types*.) + invalid_types : tuple, optional + A list of types that determine which children will *not* be + returned by this sublist. If specified, then Artists in the + sublist will never be an instance of these types. Otherwise, no + types will be excluded. + """ + self._axes = axes + self._prop_name = prop_name + self._add_name = add_name + self._type_check = lambda artist: ( + (not valid_types or isinstance(artist, valid_types)) and + (not invalid_types or not isinstance(artist, invalid_types)) + ) + + def __repr__(self): + return f'' + + def __len__(self): + return sum(self._type_check(artist) + for artist in self._axes._children) + + def __iter__(self): + for artist in self._axes._children: + if self._type_check(artist): + yield artist + + def __getitem__(self, key): + return [artist + for artist in self._axes._children + if self._type_check(artist)][key] + + def insert(self, index, item): + _api.warn_deprecated( + '3.5', + name=f'modification of the Axes.{self._prop_name}', + obj_type='property', + alternative=f'Axes.{self._add_name}') + try: + index = self._axes._children.index(self[index]) + except IndexError: + index = None + getattr(self._axes, self._add_name)(item) + if index is not None: + # Move new item to the specified index, if there's something to + # put it before. + self._axes._children[index:index] = self._axes._children[-1:] + del self._axes._children[-1] + + def __setitem__(self, key, item): + _api.warn_deprecated( + '3.5', + name=f'modification of the Axes.{self._prop_name}', + obj_type='property', + alternative=f'Artist.remove() and Axes.f{self._add_name}') + del self[key] + if isinstance(key, slice): + key = key.start + if not np.iterable(item): + self.insert(key, item) + return + + try: + index = self._axes._children.index(self[key]) + except IndexError: + index = None + for i, artist in enumerate(item): + getattr(self._axes, self._add_name)(artist) + if index is not None: + # Move new items to the specified index, if there's something + # to put it before. + i = -(i + 1) + self._axes._children[index:index] = self._axes._children[i:] + del self._axes._children[i:] + + def __delitem__(self, key): + _api.warn_deprecated( + '3.5', + name=f'modification of the Axes.{self._prop_name}', + obj_type='property', + alternative='Artist.remove()') + if isinstance(key, slice): + for artist in self[key]: + artist.remove() + else: + self[key].remove() + + @property + def artists(self): + return self.ArtistList(self, 'artists', 'add_artist', invalid_types=( + mcoll.Collection, mimage.AxesImage, mlines.Line2D, mpatches.Patch, + mtable.Table, mtext.Text)) + + @property + def collections(self): + return self.ArtistList(self, 'collections', 'add_collection', + valid_types=mcoll.Collection) + + @property + def images(self): + return self.ArtistList(self, 'images', 'add_image', + valid_types=mimage.AxesImage) + + @property + def lines(self): + return self.ArtistList(self, 'lines', 'add_line', + valid_types=mlines.Line2D) + + @property + def patches(self): + return self.ArtistList(self, 'patches', 'add_patch', + valid_types=mpatches.Patch) + + @property + def tables(self): + return self.ArtistList(self, 'tables', 'add_table', + valid_types=mtable.Table) + + @property + def texts(self): + return self.ArtistList(self, 'texts', 'add_text', + valid_types=mtext.Text) + def clear(self): """Clear the axes.""" self.cla() @@ -1981,10 +2128,13 @@ def _sci(self, im): `~.pyplot.viridis`, and other functions such as `~.pyplot.clim`. The current image is an attribute of the current axes. """ + _api.check_isinstance( + (mpl.contour.ContourSet, mcoll.Collection, mimage.AxesImage), + im=im) if isinstance(im, mpl.contour.ContourSet): - if im.collections[0] not in self.collections: + if im.collections[0] not in self._children: raise ValueError("ContourSet must be in current Axes") - elif im not in self.images and im not in self.collections: + elif im not in self._children: raise ValueError("Argument must be an image, collection, or " "ContourSet in this Axes") self._current_image = im @@ -2001,15 +2151,27 @@ def has_data(self): need to be updated, and may not actually be useful for anything. """ - return ( - len(self.collections) + - len(self.images) + - len(self.lines) + - len(self.patches)) > 0 + return any(isinstance(a, (mcoll.Collection, mimage.AxesImage, + mlines.Line2D, mpatches.Patch)) + for a in self._children) + + def _deprecate_noninstance(self, _name, _types, **kwargs): + """ + For each *key, value* pair in *kwargs*, check that *value* is an + instance of one of *_types*; if not, raise an appropriate deprecation. + """ + for key, value in kwargs.items(): + if not isinstance(value, _types): + _api.warn_deprecated( + '3.5', name=_name, + message=f'Passing argument *{key}* of unexpected type ' + f'{type(value).__qualname__} to %(name)s which only ' + f'accepts {_types} is deprecated since %(since)s and will ' + 'become an error %(removal)s.') def add_artist(self, a): """ - Add an `~.Artist` to the axes, and return the artist. + Add an `~.Artist` to the Axes; return the artist. Use `add_artist` only for artists for which there is no dedicated "add" method; and if necessary, use a method such as `update_datalim` @@ -2021,8 +2183,8 @@ def add_artist(self, a): ``ax.transData``. """ a.axes = self - self.artists.append(a) - a._remove_method = self.artists.remove + self._children.append(a) + a._remove_method = self._children.remove self._set_artist_props(a) a.set_clip_path(self.patch) self.stale = True @@ -2048,13 +2210,15 @@ def add_child_axes(self, ax): def add_collection(self, collection, autolim=True): """ - Add a `~.Collection` to the axes' collections; return the collection. + Add a `~.Collection` to the Axes; return the collection. """ + self._deprecate_noninstance('add_collection', mcoll.Collection, + collection=collection) label = collection.get_label() if not label: - collection.set_label('_collection%d' % len(self.collections)) - self.collections.append(collection) - collection._remove_method = self.collections.remove + collection.set_label(f'_child{len(self._children)}') + self._children.append(collection) + collection._remove_method = self._children.remove self._set_artist_props(collection) if collection.get_clip_path() is None: @@ -2080,13 +2244,14 @@ def add_collection(self, collection, autolim=True): def add_image(self, image): """ - Add an `~.AxesImage` to the axes' images; return the image. + Add an `~.AxesImage` to the Axes; return the image. """ + self._deprecate_noninstance('add_image', mimage.AxesImage, image=image) self._set_artist_props(image) if not image.get_label(): - image.set_label('_image%d' % len(self.images)) - self.images.append(image) - image._remove_method = self.images.remove + image.set_label(f'_child{len(self._children)}') + self._children.append(image) + image._remove_method = self._children.remove self.stale = True return image @@ -2096,27 +2261,29 @@ def _update_image_limits(self, image): def add_line(self, line): """ - Add a `.Line2D` to the axes' lines; return the line. + Add a `.Line2D` to the Axes; return the line. """ + self._deprecate_noninstance('add_line', mlines.Line2D, line=line) self._set_artist_props(line) if line.get_clip_path() is None: line.set_clip_path(self.patch) self._update_line_limits(line) if not line.get_label(): - line.set_label('_line%d' % len(self.lines)) - self.lines.append(line) - line._remove_method = self.lines.remove + line.set_label(f'_child{len(self._children)}') + self._children.append(line) + line._remove_method = self._children.remove self.stale = True return line def _add_text(self, txt): """ - Add a `~.Text` to the axes' texts; return the text. + Add a `~.Text` to the Axes; return the text. """ + self._deprecate_noninstance('_add_text', mtext.Text, txt=txt) self._set_artist_props(txt) - self.texts.append(txt) - txt._remove_method = self.texts.remove + self._children.append(txt) + txt._remove_method = self._children.remove self.stale = True return txt @@ -2166,14 +2333,15 @@ def _update_line_limits(self, line): def add_patch(self, p): """ - Add a `~.Patch` to the axes' patches; return the patch. + Add a `~.Patch` to the Axes; return the patch. """ + self._deprecate_noninstance('add_patch', mpatches.Patch, p=p) self._set_artist_props(p) if p.get_clip_path() is None: p.set_clip_path(self.patch) self._update_patch_limits(p) - self.patches.append(p) - p._remove_method = self.patches.remove + self._children.append(p) + p._remove_method = self._children.remove return p def _update_patch_limits(self, patch): @@ -2206,12 +2374,13 @@ def _update_patch_limits(self, patch): def add_table(self, tab): """ - Add a `~.Table` to the axes' tables; return the table. + Add a `~.Table` to the Axes; return the table. """ + self._deprecate_noninstance('add_table', mtable.Table, tab=tab) self._set_artist_props(tab) - self.tables.append(tab) + self._children.append(tab) tab.set_clip_path(self.patch) - tab._remove_method = self.tables.remove + tab._remove_method = self._children.remove return tab def add_container(self, container): @@ -2254,17 +2423,14 @@ def relim(self, visible_only=False): self.dataLim.set_points(mtransforms.Bbox.null().get_points()) self.ignore_existing_data_limits = True - for line in self.lines: - if not visible_only or line.get_visible(): - self._update_line_limits(line) - - for p in self.patches: - if not visible_only or p.get_visible(): - self._update_patch_limits(p) - - for image in self.images: - if not visible_only or image.get_visible(): - self._update_image_limits(image) + for artist in self._children: + if not visible_only or artist.get_visible(): + if isinstance(artist, mlines.Line2D): + self._update_line_limits(artist) + elif isinstance(artist, mpatches.Patch): + self._update_patch_limits(artist) + elif isinstance(artist, mimage.AxesImage): + self._update_image_limits(artist) def update_datalim(self, xys, updatex=True, updatey=True): """ @@ -2672,21 +2838,21 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): x_stickies = y_stickies = np.array([]) if self.use_sticky_edges: # Only iterate over axes and artists if needed. The check for - # ``hasattr(ax, "lines")`` is necessary because this can be called - # very early in the axes init process (e.g., for twin axes) when - # these attributes don't even exist yet, in which case + # ``hasattr(ax, "_children")`` is necessary because this can be + # called very early in the axes init process (e.g., for twin axes) + # when these attributes don't even exist yet, in which case # `get_children` would raise an AttributeError. if self._xmargin and scalex and self._autoscaleXon: x_stickies = np.sort(np.concatenate([ artist.sticky_edges.x for ax in self._shared_x_axes.get_siblings(self) - if hasattr(ax, "lines") + if hasattr(ax, "_children") for artist in ax.get_children()])) if self._ymargin and scaley and self._autoscaleYon: y_stickies = np.sort(np.concatenate([ artist.sticky_edges.y for ax in self._shared_y_axes.get_siblings(self) - if hasattr(ax, "lines") + if hasattr(ax, "_children") for artist in ax.get_children()])) if self.get_xscale() == 'log': x_stickies = x_stickies[x_stickies > 0] @@ -2894,8 +3060,9 @@ def draw(self, renderer=None, inframe=False): artists.remove(self._right_title) if not self.figure.canvas.is_saving(): - artists = [a for a in artists - if not a.get_animated() or a in self.images] + artists = [ + a for a in artists + if not a.get_animated() or isinstance(a, mimage.AxesImage)] artists = sorted(artists, key=attrgetter('zorder')) # rasterize artists with negative zorder @@ -4324,16 +4491,10 @@ def format_deltas(key, dx, dy): def get_children(self): # docstring inherited. return [ - *self.collections, - *self.patches, - *self.lines, - *self.texts, - *self.artists, + *self._children, *self.spines.values(), *self._get_axis_list(), self.title, self._left_title, self._right_title, - *self.tables, - *self.images, *self.child_axes, *([self.legend_] if self.legend_ is not None else []), self.patch, diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 79c98b9355ee..11125004451c 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -35,9 +35,9 @@ from matplotlib.lines import Line2D from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, StepPatch) -from matplotlib.collections import (LineCollection, RegularPolyCollection, - CircleCollection, PathCollection, - PolyCollection) +from matplotlib.collections import ( + Collection, CircleCollection, LineCollection, PathCollection, + PolyCollection, RegularPolyCollection) from matplotlib.transforms import Bbox, BboxBase, TransformedBbox from matplotlib.transforms import BboxTransformTo, BboxTransformFrom @@ -826,18 +826,23 @@ def _auto_legend_data(self): List of (x, y) offsets of all collection. """ assert self.isaxes # always holds, as this is only called internally - ax = self.parent - lines = [line.get_transform().transform_path(line.get_path()) - for line in ax.lines] - bboxes = [patch.get_bbox().transformed(patch.get_data_transform()) - if isinstance(patch, Rectangle) else - patch.get_path().get_extents(patch.get_transform()) - for patch in ax.patches] + bboxes = [] + lines = [] offsets = [] - for handle in ax.collections: - _, transOffset, hoffsets, _ = handle._prepare_points() - for offset in transOffset.transform(hoffsets): - offsets.append(offset) + for artist in self.parent._children: + if isinstance(artist, Line2D): + lines.append( + artist.get_transform().transform_path(artist.get_path())) + elif isinstance(artist, Rectangle): + bboxes.append( + artist.get_bbox().transformed(artist.get_data_transform())) + elif isinstance(artist, Patch): + bboxes.append( + artist.get_path().get_extents(artist.get_transform())) + elif isinstance(artist, Collection): + _, transOffset, hoffsets, _ = artist._prepare_points() + for offset in transOffset.transform(hoffsets): + offsets.append(offset) return bboxes, lines, offsets def get_children(self): @@ -1114,13 +1119,17 @@ def _get_legend_handles(axs, legend_handler_map=None): """ handles_original = [] for ax in axs: - handles_original += (ax.lines + ax.patches + - ax.collections + ax.containers) + handles_original += [ + *(a for a in ax._children + if isinstance(a, (Line2D, Patch, Collection))), + *ax.containers] # support parasite axes: if hasattr(ax, 'parasites'): for axx in ax.parasites: - handles_original += (axx.lines + axx.patches + - axx.collections + axx.containers) + handles_original += [ + *(a for a in axx._children + if isinstance(a, (Line2D, Patch, Collection))), + *axx.containers] handler_map = Legend.get_default_handler_map() diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 7fb8a1dde3d4..4d79e7e02999 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -210,7 +210,6 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, p = patches.FancyArrowPatch( arrow_tail, arrow_head, transform=transform, **arrow_kw) - axes.add_patch(p) arrows.append(p) lc = mcollections.LineCollection( @@ -222,9 +221,13 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, lc.set_cmap(cmap) lc.set_norm(norm) axes.add_collection(lc) - axes.autoscale_view() ac = matplotlib.collections.PatchCollection(arrows) + # Adding the collection itself is broken; see #2341. + for p in arrows: + axes.add_patch(p) + + axes.autoscale_view() stream_container = StreamplotSet(lc, ac) return stream_container diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png index e1aafcd346ed..32c9229da00a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_custompatchartist.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png b/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png index 8b6a49c84dc4..e41e21f3e50c 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png and b/lib/matplotlib/tests/baseline_images/test_axes/bxp_patchartist.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf index 7930f6d68249..b0fb8a4af7f2 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png index aba46e19e727..fbb827bbefa5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg index 427ab827f4a1..9e56f5ed36bb 100644 --- a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg +++ b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-07T20:03:21.839839 + image/svg+xml + + + Matplotlib v3.3.0rc1.post625.dev0+gf3ab37aad, https://matplotlib.org/ + + + + + - + @@ -38,191 +49,191 @@ C -2.000462 -1.161816 -2.236068 -0.593012 -2.236068 0 C -2.236068 0.593012 -2.000462 1.161816 -1.581139 1.581139 C -1.161816 2.000462 -0.593012 2.236068 0 2.236068 z -" id="m27675ac663" style="stroke:#000000;"/> +" id="ma203e0a9bc" style="stroke:#000000;"/> - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" id="m8863516224" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + +" id="m6db69444d4" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - +" id="m58dade9745" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mc946a5ae82" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -356,104 +367,104 @@ L 0 4 +" id="m8b2567c4af" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m580a4f3bfe" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -477,144 +488,149 @@ z - - + + - + - - + + - + - - - - - +" id="DejaVuSans-65" transform="scale(0.015625)"/> + + + + @@ -633,28 +649,28 @@ L 316.669091 205.7097 - + - - - + + + @@ -718,22 +734,22 @@ L 391.117091 198.5097 - + - + - + - + @@ -753,7 +769,7 @@ L 405.517091 205.7097 - + diff --git a/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png b/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png index 4e871e6cd02c..ab68ede9f617 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png and b/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf index 640df51ac227..43daec43d648 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png index 22b08c23bd49..e889450de817 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg index 8d52765e3ae0..091a693b0e53 100644 --- a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg +++ b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-08T03:00:34.113585 + image/svg+xml + + + Matplotlib v3.3.0rc1.post628+gb07dd36f3, https://matplotlib.org/ + + + + + - + @@ -27,37 +38,37 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -487,1956 +498,1956 @@ C -2.683901 -1.55874 -3 -0.795609 -3 0 C -3 0.795609 -2.683901 1.55874 -2.12132 2.12132 C -1.55874 2.683901 -0.795609 3 0 3 z -" id="mefe0d42b30" style="stroke:#1f77b4;"/> +" id="mfb1b36b408" style="stroke:#1f77b4;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2446,45 +2457,45 @@ z +" id="m03b3b1dbf4" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + @@ -2495,2971 +2506,3790 @@ L 0 3.5 +" id="m01a773088f" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -5467,7 +6297,7 @@ L 414.72 41.472 - + diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 11091ff893ab..a44cecbe162f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7029,3 +7029,78 @@ def test_warn_ignored_scatter_kwargs(): c = plt.scatter( [0], [0], marker="+", s=500, facecolor="r", edgecolor="b" ) + + +def test_artist_sublists(): + fig, ax = plt.subplots() + lines = [ax.plot(np.arange(i, i + 5))[0] for i in range(6)] + col = ax.scatter(np.arange(5), np.arange(5)) + im = ax.imshow(np.zeros((5, 5))) + patch = ax.add_patch(mpatches.Rectangle((0, 0), 5, 5)) + text = ax.text(0, 0, 'foo') + + # Get items, which should not be mixed. + assert list(ax.collections) == [col] + assert list(ax.images) == [im] + assert list(ax.lines) == lines + assert list(ax.patches) == [patch] + assert not ax.tables + assert list(ax.texts) == [text] + + # Get items should work like lists/tuple. + assert ax.lines[0] is lines[0] + assert ax.lines[-1] is lines[-1] + with pytest.raises(IndexError, match='out of range'): + ax.lines[len(lines) + 1] + + # Deleting items (multiple or single) should warn. + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + del ax.lines[-1] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + del ax.lines[-1:] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + del ax.lines[1:] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + del ax.lines[0] + + # Lists should be empty after removing items. + col.remove() + assert not ax.collections + im.remove() + assert not ax.images + patch.remove() + assert not ax.patches + text.remove() + assert not ax.texts + + # Everything else should remain empty. + assert not ax.lines + assert not ax.tables + + # Adding items should warn. + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + ax.lines.append(lines[-2]) + assert list(ax.lines) == [lines[-2]] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + ax.lines.append(lines[-1]) + assert list(ax.lines) == lines[-2:] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + ax.lines.insert(-2, lines[0]) + assert list(ax.lines) == [lines[0], lines[-2], lines[-1]] + + # Modifying items should warn. + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + ax.lines[0] = lines[0] + assert list(ax.lines) == [lines[0], lines[-2], lines[-1]] + with pytest.warns(MatplotlibDeprecationWarning, + match='modification of the Axes.lines property'): + ax.lines[1:1] = lines[1:-2] + assert list(ax.lines) == lines diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index cebf26ea066e..2fd81ac22cbb 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -126,8 +126,8 @@ def test_alpha_rcparam(): def test_fancy(): # using subplot triggers some offsetbox functionality untested elsewhere plt.subplot(121) - plt.scatter(np.arange(10), np.arange(10, 0, -1), label='XX\nXX') plt.plot([5] * 10, 'o--', label='XX') + plt.scatter(np.arange(10), np.arange(10, 0, -1), label='XX\nXX') plt.errorbar(np.arange(10), np.arange(10), xerr=0.5, yerr=0.5, label='XX') plt.legend(loc="center left", bbox_to_anchor=[1.0, 0.5], diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index c54fbe5faea1..bc1730e84956 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -148,18 +148,18 @@ def test_patch_alpha_coloring(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch(cut_star1, - linewidth=5, linestyle='dashdot', - facecolor=(1, 0, 0, 0.5), - edgecolor=(0, 0, 1, 0.75)) - ax.add_patch(patch) - col = mcollections.PathCollection([cut_star2], linewidth=5, linestyles='dashdot', facecolor=(1, 0, 0, 0.5), edgecolor=(0, 0, 1, 0.75)) ax.add_collection(col) + patch = mpatches.PathPatch(cut_star1, + linewidth=5, linestyle='dashdot', + facecolor=(1, 0, 0, 0.5), + edgecolor=(0, 0, 1, 0.75)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) @@ -178,13 +178,6 @@ def test_patch_alpha_override(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch(cut_star1, - linewidth=5, linestyle='dashdot', - alpha=0.25, - facecolor=(1, 0, 0, 0.5), - edgecolor=(0, 0, 1, 0.75)) - ax.add_patch(patch) - col = mcollections.PathCollection([cut_star2], linewidth=5, linestyles='dashdot', alpha=0.25, @@ -192,6 +185,13 @@ def test_patch_alpha_override(): edgecolor=(0, 0, 1, 0.75)) ax.add_collection(col) + patch = mpatches.PathPatch(cut_star1, + linewidth=5, linestyle='dashdot', + alpha=0.25, + facecolor=(1, 0, 0, 0.5), + edgecolor=(0, 0, 1, 0.75)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) @@ -217,18 +217,18 @@ def test_patch_custom_linestyle(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch( - cut_star1, - linewidth=5, linestyle=(0, (5, 7, 10, 7)), - facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) - ax.add_patch(patch) - col = mcollections.PathCollection( [cut_star2], linewidth=5, linestyles=[(0, (5, 7, 10, 7))], facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) ax.add_collection(col) + patch = mpatches.PathPatch( + cut_star1, + linewidth=5, linestyle=(0, (5, 7, 10, 7)), + facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 75702a608219..d073e8c13dea 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -69,16 +69,13 @@ def _as_mpl_transform(self, axes): mtransforms.Affine2D().scale(10).get_matrix()) -@image_comparison(['pre_transform_data'], - tol=0.08, remove_text=True, style='mpl20') +@image_comparison(['pre_transform_data'], remove_text=True, style='mpl20', + tol=0.05) def test_pre_transform_plotting(): # a catch-all for as many as possible plot layouts which handle # pre-transforming the data NOTE: The axis range is important in this # plot. It should be x10 what the data suggests it should be - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - ax = plt.axes() times10 = mtransforms.Affine2D().scale(10) @@ -97,9 +94,8 @@ def test_pre_transform_plotting(): u = 2*np.sin(x) + np.cos(y[:, np.newaxis]) v = np.sin(x) - np.cos(y[:, np.newaxis]) - df = 25. / 30. # Compatibility factor for old test image ax.streamplot(x, y, u, v, transform=times10 + ax.transData, - density=(df, df), linewidth=u**2 + v**2) + linewidth=np.hypot(u, v)) # reduce the vector data down a bit for barb and quiver plotting x, y = x[::3], y[::3] diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index b7ee444077f7..eea567ff5fd5 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -2,6 +2,7 @@ from matplotlib import _api import matplotlib.artist as martist +import matplotlib.image as mimage import matplotlib.transforms as mtransforms from matplotlib.axes import subplot_class_factory from matplotlib.transforms import Bbox @@ -24,9 +25,18 @@ def cla(self): self._get_lines = self._parent_axes._get_lines def get_images_artists(self): - artists = {a for a in self.get_children() if a.get_visible()} - images = {a for a in self.images if a.get_visible()} - return list(images), list(artists - images) + artists = [] + images = [] + + for a in self.get_children(): + if not a.get_visible(): + continue + if isinstance(a, mimage.AxesImage): + images.append(a) + else: + artists.append(a) + + return images, artists def pick(self, mouseevent): # This most likely goes to Artist.pick (depending on axes_class given @@ -203,8 +213,7 @@ def _get_legend_handles(self, legend_handler_map=None): def draw(self, renderer): - orig_artists = list(self.artists) - orig_images = list(self.images) + orig_children_len = len(self._children) if hasattr(self, "get_axes_locator"): locator = self.get_axes_locator() @@ -222,12 +231,11 @@ def draw(self, renderer): for ax in self.parasites: ax.apply_aspect(rect) images, artists = ax.get_images_artists() - self.images.extend(images) - self.artists.extend(artists) + self._children.extend(images) + self._children.extend(artists) super().draw(renderer) - self.artists = orig_artists - self.images = orig_images + self._children = self._children[:orig_children_len] def cla(self): for ax in self.parasites: diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 8e30752d71cb..4dd8f4961fe1 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -19,11 +19,14 @@ import numpy as np -from matplotlib import _api, artist, cbook, docstring +from matplotlib import _api, cbook, docstring +import matplotlib.artist as martist import matplotlib.axes as maxes import matplotlib.collections as mcoll import matplotlib.colors as mcolors +import matplotlib.image as mimage import matplotlib.lines as mlines +import matplotlib.patches as mpatches import matplotlib.scale as mscale import matplotlib.container as mcontainer import matplotlib.transforms as mtransforms @@ -417,7 +420,7 @@ def apply_aspect(self, position=None): pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') - @artist.allow_rasterization + @martist.allow_rasterization def draw(self, renderer): self._unstale_viewLim() @@ -479,26 +482,27 @@ def do_3d_projection(artist): "%(since)s and will be removed %(removal)s.") return artist.do_3d_projection(renderer) + collections_and_patches = ( + artist for artist in self._children + if isinstance(artist, (mcoll.Collection, mpatches.Patch))) if self.computed_zorder: # Calculate projection of collections and patches and zorder # them. Make sure they are drawn above the grids. zorder_offset = max(axis.get_zorder() for axis in self._get_axis_list()) + 1 - for i, col in enumerate( - sorted(self.collections, - key=do_3d_projection, - reverse=True)): - col.zorder = zorder_offset + i - for i, patch in enumerate( - sorted(self.patches, - key=do_3d_projection, - reverse=True)): - patch.zorder = zorder_offset + i + collection_zorder = patch_zorder = zorder_offset + for artist in sorted(collections_and_patches, + key=do_3d_projection, + reverse=True): + if isinstance(artist, mcoll.Collection): + artist.zorder = collection_zorder + collection_zorder += 1 + elif isinstance(artist, mpatches.Patch): + artist.zorder = patch_zorder + patch_zorder += 1 else: - for col in self.collections: - col.do_3d_projection() - for patch in self.patches: - patch.do_3d_projection() + for artist in collections_and_patches: + artist.do_3d_projection() if self._axis3don: # Draw panes first @@ -731,10 +735,15 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, # This method looks at the rectangular volume (see above) # of data and decides how to scale the view portal to fit it. if tight is None: - # if image data only just use the datalim - _tight = self._tight or ( - len(self.images) > 0 - and len(self.lines) == len(self.patches) == 0) + _tight = self._tight + if not _tight: + # if image data only just use the datalim + for artist in self._children: + if isinstance(artist, mimage.AxesImage): + _tight = True + elif isinstance(artist, (mlines.Line2D, mpatches.Patch)): + _tight = False + break else: _tight = self._tight = bool(tight) @@ -2072,7 +2081,7 @@ def _3d_extend_contour(self, cset, stride=5): self.add_collection3d(polycol) for col in colls: - self.collections.remove(col) + col.remove() def add_contour_set( self, cset, extend3d=False, stride=5, zdir='z', offset=None): @@ -3434,7 +3443,7 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', stem3D = stem -docstring.interpd.update(Axes3D_kwdoc=artist.kwdoc(Axes3D)) +docstring.interpd.update(Axes3D_kwdoc=martist.kwdoc(Axes3D)) docstring.dedent_interpd(Axes3D.__init__)