diff --git a/doc/api/next_api_changes/deprecations/20237-TH.rst b/doc/api/next_api_changes/deprecations/20237-TH.rst new file mode 100644 index 000000000000..2120fb9b9bef --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20237-TH.rst @@ -0,0 +1,17 @@ +QuadMesh signature +~~~~~~~~~~~~~~~~~~ +The ``QuadMesh`` signature :: + + def __init__(meshWidth, meshHeight, coordinates, + antialiased=True, shading=False, **kwargs) + +is deprecated and replaced by the new signature :: + + def __init__(coordinates, *, antialiased=True, shading=False, **kwargs) + +In particular: + +- *coordinates* must now be a (M, N, 2) array-like. Previously, the grid + shape was separately specified as (*meshHeight* + 1, *meshWidth* + 1) and + *coordinates* could be an array-like of any shape with M * N * 2 elements. +- all parameters except *coordinates* are keyword-only now. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index ee4c2b5e371b..2fe936673414 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6164,16 +6164,12 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, X, Y, C, shading = self._pcolorargs('pcolormesh', *args, shading=shading, kwargs=kwargs) - Ny, Nx = X.shape - X = X.ravel() - Y = Y.ravel() - - # convert to one dimensional arrays + coords = np.stack([X, Y], axis=-1) + # convert to one dimensional array C = C.ravel() - coords = np.column_stack((X, Y)).astype(float, copy=False) - collection = mcoll.QuadMesh(Nx - 1, Ny - 1, coords, - antialiased=antialiased, shading=shading, - **kwargs) + + collection = mcoll.QuadMesh( + coords, antialiased=antialiased, shading=shading, **kwargs) snap = kwargs.get('snap', rcParams['pcolormesh.snap']) collection.set_snap(snap) collection.set_alpha(alpha) @@ -6184,6 +6180,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, self.grid(False) + coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y + # Transform from native to data coordinates? t = collection._transform if (not isinstance(t, mtransforms.Transform) and @@ -6360,7 +6358,7 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, else: raise ValueError("C must be 2D or 3D") collection = mcoll.QuadMesh( - nc, nr, coords, **qm_kwargs, + coords, **qm_kwargs, alpha=alpha, cmap=cmap, norm=norm, antialiased=False, edgecolors="none") self.add_collection(collection, autolim=False) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index d02607cc5052..3061280eb642 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -9,15 +9,17 @@ line segments). """ +import inspect import math from numbers import Number +import warnings + import numpy as np import matplotlib as mpl from . import (_api, _path, artist, cbook, cm, colors as mcolors, docstring, hatch as mhatch, lines as mlines, path as mpath, transforms) from ._enums import JoinStyle, CapStyle -import warnings # "color" is excluded; it is a compound setter, and its docstring differs @@ -1966,6 +1968,35 @@ class QuadMesh(Collection): """ Class for the efficient drawing of a quadrilateral mesh. + A quadrilateral mesh is a grid of M by N adjacent qudrilaterals that are + defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is + defind by the vertices :: + + (m+1, n) ----------- (m+1, n+1) + / / + / / + / / + (m, n) -------- (m, n+1) + + The mesh need not be regular and the polygons need not be convex. + + Parameters + ---------- + coordinates : (M+1, N+1, 2) array-like + The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates + of vertex (m, n). + + antialiased : bool, default: True + + shading : {'flat', 'gouraud'}, default: 'flat' + + Notes + ----- + There exists a deprecated API version ``QuadMesh(M, N, coords)``, where + the dimensions are given explicitly and ``coords`` is a (M*N, 2) + array-like. This has been deprecated in Matplotlib 3.5. The following + describes the semantics of this deprecated API. + A quadrilateral mesh consists of a grid of vertices. The dimensions of this array are (*meshWidth* + 1, *meshHeight* + 1). Each vertex in the mesh has a different set of "mesh coordinates" @@ -1989,22 +2020,55 @@ class QuadMesh(Collection): vertex at mesh coordinates (0, 0), then the one at (0, 1), then at (0, 2) .. (0, meshWidth), (1, 0), (1, 1), and so on. - *shading* may be 'flat', or 'gouraud' """ - def __init__(self, meshWidth, meshHeight, coordinates, - antialiased=True, shading='flat', **kwargs): + def __init__(self, *args, **kwargs): + # signature deprecation since="3.5": Change to new signature after the + # deprecation has expired. Also remove setting __init__.__signature__, + # and remove the Notes from the docstring. + # + # We use lambdas to parse *args, **kwargs through the respective old + # and new signatures. + try: + # Old signature: + # The following raises a TypeError iif the args don't match. + w, h, coords, antialiased, shading, kwargs = ( + lambda meshWidth, meshHeight, coordinates, antialiased=True, + shading=False, **kwargs: + (meshWidth, meshHeight, coordinates, antialiased, shading, + kwargs))(*args, **kwargs) + except TypeError as exc: + # New signature: + # If the following raises a TypeError (i.e. args don't match), + # just let it propagate. + coords, antialiased, shading, kwargs = ( + lambda coordinates, antialiased=True, shading=False, **kwargs: + (coordinates, antialiased, shading, kwargs))(*args, **kwargs) + coords = np.asarray(coords, np.float64) + else: # The old signature matched. + _api.warn_deprecated( + "3.5", + message="This usage of Quadmesh is deprecated: Parameters " + "meshWidth and meshHeights will be removed; " + "coordinates must be 2D; all parameters except " + "coordinates will be keyword-only.") + coords = np.asarray(coords, np.float64).reshape((h + 1, w + 1, 2)) + # end of signature deprecation code + super().__init__(**kwargs) - self._meshWidth = meshWidth - self._meshHeight = meshHeight - # By converting to floats now, we can avoid that on every draw. - self._coordinates = np.asarray(coordinates, float).reshape( - (meshHeight + 1, meshWidth + 1, 2)) + _api.check_shape((None, None, 2), coordinates=coords) + self._coordinates = coords + self._meshWidth = self._coordinates.shape[1] - 1 + self._meshHeight = self._coordinates.shape[0] - 1 self._antialiased = antialiased self._shading = shading self._bbox = transforms.Bbox.unit() - self._bbox.update_from_data_xy(coordinates.reshape( - ((meshWidth + 1) * (meshHeight + 1), 2))) + self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2)) + + # Only needed during signature deprecation + __init__.__signature__ = inspect.signature( + lambda self, coordinates, *, + antialiased=True, shading='flat', **kwargs: None) def get_paths(self): if self._paths is None: diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index c56aa93ed9aa..0f6200e6bad1 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -714,6 +714,78 @@ def test_singleton_autolim(): np.testing.assert_allclose(ax.get_xlim(), [-0.06, 0.06]) +@pytest.mark.parametrize('flat_ref, kwargs', [ + (True, {}), + (False, {}), + (True, dict(antialiased=False)), + (False, dict(transform='__initialization_delayed__')), +]) +@check_figures_equal(extensions=['png']) +def test_quadmesh_deprecated_signature( + fig_test, fig_ref, flat_ref, kwargs): + # test that the new and old quadmesh signature produce the same results + # remove when the old QuadMesh.__init__ signature expires (v3.5+2) + from matplotlib.collections import QuadMesh + + x = [0, 1, 2, 3.] + y = [1, 2, 3.] + X, Y = np.meshgrid(x, y) + X += 0.2 * Y + coords = np.stack([X, Y], axis=-1) + assert coords.shape == (3, 4, 2) + C = np.linspace(0, 2, 12).reshape(3, 4) + + ax = fig_test.add_subplot() + ax.set(xlim=(0, 5), ylim=(0, 4)) + if 'transform' in kwargs: + kwargs['transform'] = mtransforms.Affine2D().scale(1.2) + ax.transData + qmesh = QuadMesh(coords, **kwargs) + qmesh.set_array(C) + ax.add_collection(qmesh) + + ax = fig_ref.add_subplot() + ax.set(xlim=(0, 5), ylim=(0, 4)) + if 'transform' in kwargs: + kwargs['transform'] = mtransforms.Affine2D().scale(1.2) + ax.transData + with pytest.warns(MatplotlibDeprecationWarning): + qmesh = QuadMesh(4 - 1, 3 - 1, + coords.copy().reshape(-1, 2) if flat_ref else coords, + **kwargs) + qmesh.set_array(C.flatten() if flat_ref else C) + ax.add_collection(qmesh) + + +@check_figures_equal(extensions=['png']) +def test_quadmesh_deprecated_positional(fig_test, fig_ref): + # test that positional parameters are still accepted with the old signature + # and work correctly + # remove when the old QuadMesh.__init__ signature expires (v3.5+2) + from matplotlib.collections import QuadMesh + + x = [0, 1, 2, 3.] + y = [1, 2, 3.] + X, Y = np.meshgrid(x, y) + X += 0.2 * Y + coords = np.stack([X, Y], axis=-1) + assert coords.shape == (3, 4, 2) + coords_flat = coords.copy().reshape(-1, 2) + C = np.linspace(0, 2, 12).reshape(3, 4) + + ax = fig_test.add_subplot() + ax.set(xlim=(0, 5), ylim=(0, 4)) + qmesh = QuadMesh(coords, antialiased=False, shading='gouraud') + qmesh.set_array(C) + ax.add_collection(qmesh) + + ax = fig_ref.add_subplot() + ax.set(xlim=(0, 5), ylim=(0, 4)) + with pytest.warns(MatplotlibDeprecationWarning): + qmesh = QuadMesh(4 - 1, 3 - 1, coords.copy().reshape(-1, 2), + False, 'gouraud') + qmesh.set_array(C) + ax.add_collection(qmesh) + + def test_quadmesh_set_array(): x = np.arange(4) y = np.arange(4)