From 4ca0d89486d5e3da36b5a550a8ae13ea3bd0d24e Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 23 Sep 2021 17:14:23 -0600 Subject: [PATCH 1/4] FIX: Update the shading keyword in pcolormesh With wrapped coordinates and shading=nearest/auto, we are updating the points before passing into Matplotlib, so we also need to update the shading keyword before passing into matplotlib. --- lib/cartopy/mpl/geoaxes.py | 11 +++++++---- lib/cartopy/tests/mpl/test_mpl_integration.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index 4fc11b005..db9fa99bb 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -1789,7 +1789,7 @@ def pcolormesh(self, *args, **kwargs): """ # Add in an argument checker to handle Matplotlib's potential # interpolation when coordinate wraps are involved - args = self._wrap_args(*args, **kwargs) + args, kwargs = self._wrap_args(*args, **kwargs) result = super().pcolormesh(*args, **kwargs) # Wrap the quadrilaterals if necessary result = self._wrap_quadmesh(result, **kwargs) @@ -1811,8 +1811,11 @@ def _wrap_args(self, *args, **kwargs): if not (kwargs.get('shading', default_shading) in ('nearest', 'auto') and len(args) == 3 and getattr(kwargs.get('transform'), '_wrappable', False)): - return args + return args, kwargs + # We have changed the shading from nearest/auto to flat + # due to the addition of an extra coordinate + kwargs['shading'] = 'flat' X = np.asanyarray(args[0]) Y = np.asanyarray(args[1]) nrows, ncols = np.asanyarray(args[2]).shape @@ -1848,7 +1851,7 @@ def _interp_grid(X, wrap=0): X = _interp_grid(X.T, wrap=xwrap).T Y = _interp_grid(Y.T).T - return (X, Y, args[2]) + return (X, Y, args[2]), kwargs def _wrap_quadmesh(self, collection, **kwargs): """ @@ -1974,7 +1977,7 @@ def pcolor(self, *args, **kwargs): """ # Add in an argument checker to handle Matplotlib's potential # interpolation when coordinate wraps are involved - args = self._wrap_args(*args, **kwargs) + args, kwargs = self._wrap_args(*args, **kwargs) result = super().pcolor(*args, **kwargs) # Update the datalim for this pcolor. diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index add8d6b16..04e94d6c9 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -684,6 +684,21 @@ def test_pcolormesh_wrap_set_array(): return ax.figure +@pytest.mark.parametrize('shading', ['auto', 'nearest']) +def test_pcolormesh_shading(shading): + # Smoke test that auto/nearest shading get + # properly set to flat shading in the call to pcolormesh + # GH issue 1889 + ax = plt.axes(projection=ccrs.PlateCarree()) + + n = 3 + x = np.arange(n)+1 + y = np.arange(n)+1 + d = np.random.rand(n, n) + + ax.pcolormesh(x, y, d, shading=shading) + + @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='quiver_plate_carree.png') def test_quiver_plate_carree(): From 1539d51ad5f91ba4caa563ec2e3b7f545eeb0376 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 23 Sep 2021 20:28:09 -0600 Subject: [PATCH 2/4] FIX: Make an attempt at handling gouraud shading in pcolormesh Gouraud shading doesn't work with pcolor() calls. However, we can at least attempt to get the shapes right so that an error isn't raised, but rather a warning indicating what the user should do. --- lib/cartopy/mpl/geoaxes.py | 24 ++++++++++++++- lib/cartopy/tests/mpl/test_mpl_integration.py | 30 ++++++++++++------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index db9fa99bb..2fadd6553 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -1867,8 +1867,13 @@ def _wrap_quadmesh(self, collection, **kwargs): # Get the quadmesh data coordinates coords = collection._coordinates Ny, Nx, _ = coords.shape + if kwargs.get('shading') == 'gouraud': + # Gouraud shading has the same shape for coords and data + data_shape = Ny, Nx + else: + data_shape = Ny - 1, Nx - 1 # data array - C = collection.get_array().reshape((Ny - 1, Nx - 1)) + C = collection.get_array().reshape(data_shape) transformed_pts = self.projection.transform_points( t, coords[..., 0], coords[..., 1]) @@ -1897,6 +1902,23 @@ def _wrap_quadmesh(self, collection, **kwargs): # No wrapping needed return collection + # Wrapping with gouraud shading is error-prone. We will do our best, + # but pcolor does not handle gouraud shading, so there needs to be + # another way to handle the wrapped cells. + if kwargs.get('shading') == 'gouraud': + warnings.warn("Handling wrapped coordinates with gouraud " + "shading is likely to introduce artifacts. " + "It is recommended to remove the wrap manually " + "before calling pcolormesh.") + # With gouraud shading, we actually want an (Ny, Nx) shaped mask + gmask = np.zeros(data_shape, dtype=bool) + # If any of the cells were wrapped, apply it to all 4 corners + gmask[:-1, :-1] |= mask + gmask[1:, :-1] |= mask + gmask[1:, 1:] |= mask + gmask[:-1, 1:] |= mask + mask = gmask + # We have quadrilaterals that cross the wrap boundary # Now, we need to update the original collection with # a mask over those cells and use pcolor to draw those diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index 04e94d6c9..788b095e8 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -684,19 +684,29 @@ def test_pcolormesh_wrap_set_array(): return ax.figure -@pytest.mark.parametrize('shading', ['auto', 'nearest']) -def test_pcolormesh_shading(shading): - # Smoke test that auto/nearest shading get - # properly set to flat shading in the call to pcolormesh - # GH issue 1889 +@pytest.mark.parametrize('shading, input_size, expected', [ + pytest.param('auto', 3, 4, id='auto same size'), + pytest.param('auto', 4, 4, id='auto input larger'), + pytest.param('nearest', 3, 4, id='nearest same size'), + pytest.param('nearest', 4, 4, id='nearest input larger'), + pytest.param('flat', 4, 4, id='flat input larger'), + pytest.param('gouraud', 3, 3, id='gouraud same size') +]) +def test_pcolormesh_shading(shading, input_size, expected): + # Testing that the coordinates are all broadcast as expected with + # the various shading options + # The data shape is (3, 3) and we are changing the input shape + # based upon that ax = plt.axes(projection=ccrs.PlateCarree()) - n = 3 - x = np.arange(n)+1 - y = np.arange(n)+1 - d = np.random.rand(n, n) + x = np.arange(input_size) + y = np.arange(input_size) + d = np.zeros((3, 3)) - ax.pcolormesh(x, y, d, shading=shading) + coll = ax.pcolormesh(x, y, d, shading=shading) + # We can use coll.get_coordinates() once MPL >= 3.5 is required + # For now, we use the private variable for testing + assert coll._coordinates.shape == (expected, expected, 2) @pytest.mark.natural_earth From 6a3c6164bbd2af88b704729b12bd64f9654ec09a Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 23 Sep 2021 20:33:22 -0600 Subject: [PATCH 3/4] TST: Cleaning up test warnings for pcolormesh data shapes --- lib/cartopy/tests/mpl/test_mpl_integration.py | 2 +- lib/cartopy/tests/mpl/test_pseudo_color.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index 788b095e8..0a925d9b6 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -612,7 +612,7 @@ def test_pcolormesh_diagonal_wrap(): # and the bottom edge on the other gets wrapped properly xs = [[160, 170], [190, 200]] ys = [[-10, -10], [10, 10]] - zs = [[0, 1], [0, 1]] + zs = [[0]] ax = plt.axes(projection=ccrs.PlateCarree()) mesh = ax.pcolormesh(xs, ys, zs) diff --git a/lib/cartopy/tests/mpl/test_pseudo_color.py b/lib/cartopy/tests/mpl/test_pseudo_color.py index 35824e19d..b3664a227 100644 --- a/lib/cartopy/tests/mpl/test_pseudo_color.py +++ b/lib/cartopy/tests/mpl/test_pseudo_color.py @@ -14,7 +14,7 @@ def test_pcolormesh_partially_masked(): - data = np.ma.masked_all((40, 30)) + data = np.ma.masked_all((39, 29)) data[0:100] = 10 # Check that a partially masked data array does trigger a pcolor call. @@ -26,7 +26,7 @@ def test_pcolormesh_partially_masked(): def test_pcolormesh_invisible(): - data = np.zeros((3, 3)) + data = np.zeros((2, 2)) # Check that a fully invisible mesh doesn't fail. with mock.patch('cartopy.mpl.geoaxes.GeoAxes.pcolor') as pcolor: From f3c9315c30ac3b56f670ea31636c523105880f7c Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 23 Sep 2021 20:51:43 -0600 Subject: [PATCH 4/4] FIX: Handle shading argument with older MPL versions --- lib/cartopy/mpl/geoaxes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index 2fadd6553..5fe302639 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -2000,6 +2000,10 @@ def pcolor(self, *args, **kwargs): # Add in an argument checker to handle Matplotlib's potential # interpolation when coordinate wraps are involved args, kwargs = self._wrap_args(*args, **kwargs) + if matplotlib.__version__ < "3.3": + # MPL 3.3 introduced the shading option, and it isn't + # handled before that for pcolor calls. + kwargs.pop('shading', None) result = super().pcolor(*args, **kwargs) # Update the datalim for this pcolor.