Skip to content

Commit

Permalink
Merge pull request #1892 from greglucas/pcolor-shading-nearest
Browse files Browse the repository at this point in the history
FIX: pcolor shading with nearest
  • Loading branch information
QuLogic authored Oct 8, 2021
2 parents 78ea7f8 + f3c9315 commit 0baa5e0
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 8 deletions.
39 changes: 34 additions & 5 deletions lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,7 +1723,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)
Expand All @@ -1745,8 +1745,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
Expand Down Expand Up @@ -1782,7 +1785,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):
"""
Expand All @@ -1798,8 +1801,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])
Expand Down Expand Up @@ -1828,6 +1836,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
Expand Down Expand Up @@ -1908,7 +1933,11 @@ 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)
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.
Expand Down
27 changes: 26 additions & 1 deletion lib/cartopy/tests/mpl/test_mpl_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,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)
Expand Down Expand Up @@ -659,6 +659,31 @@ def test_pcolormesh_wrap_set_array():
return ax.figure


@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())

x = np.arange(input_size)
y = np.arange(input_size)
d = np.zeros((3, 3))

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
@pytest.mark.mpl_image_compare(filename='quiver_plate_carree.png')
def test_quiver_plate_carree():
Expand Down
4 changes: 2 additions & 2 deletions lib/cartopy/tests/mpl/test_pseudo_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down

0 comments on commit 0baa5e0

Please sign in to comment.