From 78b1fec8862c4b73eb0aba3e6a9c1dc1158f1810 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 1 Feb 2023 10:09:30 +0100 Subject: [PATCH] Deprecate QuadContourSet.allsegs, .allkinds, .tcolors, .tlinewidths. Directly construct the relevant path objects and the collections with the right properties instead of carrying around another copy of the information in raw array form. By having fewer intermediate objects lying around for a long time (as attributes of the QuadContourSet) this makes the logic easier to follow. --- .../deprecations/25138-AL.rst | 5 ++ lib/matplotlib/colorbar.py | 4 +- lib/matplotlib/contour.py | 76 ++++++++++--------- lib/matplotlib/tests/test_contour.py | 22 +++++- 4 files changed, 66 insertions(+), 41 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/25138-AL.rst diff --git a/doc/api/next_api_changes/deprecations/25138-AL.rst b/doc/api/next_api_changes/deprecations/25138-AL.rst new file mode 100644 index 000000000000..8fa510fc4c90 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/25138-AL.rst @@ -0,0 +1,5 @@ +``allsegs``, ``allkinds``, ``tcolors`` and ``tlinewidths`` attributes of `.ContourSet` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These attributes are deprecated; if required, directly retrieve the vertices +and codes of the Path objects in ``QuadContourSet.collections`` and the colors +and the linewidths of these collections. diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 14c7c1e58b9a..d3d53e3d98f8 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -774,8 +774,8 @@ def add_lines(self, *args, **kwargs): # TODO: Make colorbar lines auto-follow changes in contour lines. return self.add_lines( CS.levels, - [c[0] for c in CS.tcolors], - [t[0] for t in CS.tlinewidths], + CS.to_rgba(CS.cvalues, CS.alpha), + [coll.get_linewidths()[0] for coll in CS.collections], erase=erase) else: self, levels, colors, linewidths, erase = params.values() diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 86009bee63a7..208c426ba68e 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -3,7 +3,6 @@ """ import functools -import itertools from numbers import Integral import numpy as np @@ -767,6 +766,10 @@ def __init__(self, ax, *args, self.negative_linestyles = \ mpl.rcParams['contour.negative_linestyle'] + # The base class _process_args will update _allpaths, which gets picked + # up by _get_allpaths below. OTOH the _process_args of subclasses + # leave _allpaths as None and instead set _contour_generator. + self._allpaths = None kwargs = self._process_args(*args, **kwargs) self._process_levels() @@ -820,23 +823,7 @@ def __init__(self, ax, *args, self.norm.vmax = vmax self._process_colors() - if getattr(self, 'allsegs', None) is None: - self.allsegs, self.allkinds = self._get_allsegs_and_allkinds() - elif self.allkinds is None: - # allsegs specified in constructor may or may not have allkinds as - # well. Must ensure allkinds can be zipped below. - self.allkinds = [None] * len(self.allsegs) - - # Each entry in (allsegs, allkinds) is a list of (segs, kinds) which - # specifies a list of Paths: segs is a list of (N, 2) arrays of xy - # coordinates, kinds is a list of arrays of corresponding pathcodes. - # However, kinds can also be None; in which case all paths in that list - # are codeless. - allpaths = [ - [*map(mpath.Path, - segs, - kinds if kinds is not None else itertools.repeat(None))] - for segs, kinds in zip(self.allsegs, self.allkinds)] + allpaths = self._get_allpaths() if self.filled: if self.linewidths is not None: @@ -857,7 +844,7 @@ def __init__(self, ax, *args, for level, level_upper, paths in zip(lowers, uppers, allpaths)] else: - self.tlinewidths = tlinewidths = self._process_linewidths() + tlinewidths = self._process_linewidths() tlinestyles = self._process_linestyles() aa = self.antialiased if aa is not None: @@ -895,6 +882,15 @@ def __init__(self, ax, *args, ", ".join(map(repr, kwargs)) ) + allsegs = _api.deprecated("3.8", pending=True)(property(lambda self: [ + p.vertices for c in self.collections for p in c.get_paths()])) + allkinds = _api.deprecated("3.8", pending=True)(property(lambda self: [ + p.codes for c in self.collections for p in c.get_paths()])) + tcolors = _api.deprecated("3.8")(property(lambda self: [ + (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)])) + tlinewidths = _api.deprecated("3.8")( + property(lambda self: self._process_linewidths())) + def get_transform(self): """Return the `.Transform` instance used by this ContourSet.""" if self._transform is None: @@ -979,51 +975,60 @@ def _process_args(self, *args, **kwargs): Must set self.levels, self.zmin and self.zmax, and update axes limits. """ self.levels = args[0] - self.allsegs = args[1] - self.allkinds = args[2] if len(args) > 2 else None + allsegs = args[1] + allkinds = args[2] if len(args) > 2 else None self.zmax = np.max(self.levels) self.zmin = np.min(self.levels) + if allkinds is None: + allkinds = [[None] * len(segs) for segs in allsegs] + # Check lengths of levels and allsegs. if self.filled: - if len(self.allsegs) != len(self.levels) - 1: + if len(allsegs) != len(self.levels) - 1: raise ValueError('must be one less number of segments as ' 'levels') else: - if len(self.allsegs) != len(self.levels): + if len(allsegs) != len(self.levels): raise ValueError('must be same number of segments as levels') # Check length of allkinds. - if (self.allkinds is not None and - len(self.allkinds) != len(self.allsegs)): + if len(allkinds) != len(allsegs): raise ValueError('allkinds has different length to allsegs') # Determine x, y bounds and update axes data limits. - flatseglist = [s for seg in self.allsegs for s in seg] + flatseglist = [s for seg in allsegs for s in seg] points = np.concatenate(flatseglist, axis=0) self._mins = points.min(axis=0) self._maxs = points.max(axis=0) + # Each entry in (allsegs, allkinds) is a list of (segs, kinds) which + # specifies a list of Paths: segs is a list of (N, 2) arrays of xy + # coordinates, kinds is a list of arrays of corresponding pathcodes. + # However, kinds can also be None; in which case all paths in that list + # are codeless (this case is normalized above). + self._allpaths = [[*map(mpath.Path, segs, kinds)] + for segs, kinds in zip(allsegs, allkinds)] + return kwargs - def _get_allsegs_and_allkinds(self): - """Compute ``allsegs`` and ``allkinds`` using C extension.""" - allsegs = [] - allkinds = [] + def _get_allpaths(self): + """Compute ``allpaths`` using C extension.""" + if self._allpaths is not None: + return self._allpaths + allpaths = [] if self.filled: lowers, uppers = self._get_lowers_and_uppers() for level, level_upper in zip(lowers, uppers): vertices, kinds = \ self._contour_generator.create_filled_contour( level, level_upper) - allsegs.append(vertices) - allkinds.append(kinds) + allpaths.append([*map(mpath.Path, vertices, kinds)]) else: for level in self.levels: vertices, kinds = self._contour_generator.create_contour(level) - allsegs.append(vertices) - allkinds.append(kinds) - return allsegs, allkinds + allpaths.append([*map(mpath.Path, vertices, kinds)]) + return allpaths def _get_lowers_and_uppers(self): """ @@ -1052,7 +1057,6 @@ def changed(self): self.norm.autoscale_None(self.levels) tcolors = [(tuple(rgba),) for rgba in self.to_rgba(self.cvalues, alpha=self.alpha)] - self.tcolors = tcolors hatches = self.hatches * len(tcolors) for color, hatch, collection in zip(tcolors, hatches, self.collections): diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index d56a4c9a972a..8c38554ed45c 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -5,11 +5,12 @@ import contourpy import numpy as np from numpy.testing import ( - assert_array_almost_equal, assert_array_almost_equal_nulp) + assert_array_almost_equal, assert_array_almost_equal_nulp, assert_array_equal) import matplotlib as mpl -from matplotlib.testing.decorators import image_comparison from matplotlib import pyplot as plt, rc_context, ticker +from matplotlib._api import MatplotlibDeprecationWarning from matplotlib.colors import LogNorm, same_color +from matplotlib.testing.decorators import image_comparison import pytest @@ -365,7 +366,9 @@ def test_contour_linewidth( fig, ax = plt.subplots() X = np.arange(4*3).reshape(4, 3) cs = ax.contour(X, linewidths=call_linewidths) - assert cs.tlinewidths[0][0] == expected + assert cs.collections[0].get_linewidths()[0] == expected + with pytest.warns(MatplotlibDeprecationWarning, match="tlinewidths"): + assert cs.tlinewidths[0][0] == expected @pytest.mark.backend("pdf") @@ -722,3 +725,16 @@ def test_all_nan(): assert_array_almost_equal(plt.contour(x).levels, [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0, 2.4e-14, 5e-14, 7.5e-14, 1e-13]) + + +def test_deprecated_apis(): + cs = plt.contour(np.arange(16).reshape((4, 4))) + colls = cs.collections + with pytest.warns(PendingDeprecationWarning, match="allsegs"): + assert cs.allsegs == [p.vertices for c in colls for p in c.get_paths()] + with pytest.warns(PendingDeprecationWarning, match="allkinds"): + assert cs.allkinds == [p.codes for c in colls for p in c.get_paths()] + with pytest.warns(MatplotlibDeprecationWarning, match="tcolors"): + assert_array_equal(cs.tcolors, [c.get_edgecolor() for c in colls]) + with pytest.warns(MatplotlibDeprecationWarning, match="tlinewidths"): + assert cs.tlinewidths == [c.get_linewidth() for c in colls]