Skip to content

Commit f70e61f

Browse files
committed
FIX: Update pcolor(mesh) to create grid edges
MPL 3.3 added a new interpolation routine to automatically create edges for the grids when the input X/Y are the same shape as the color data. This causes issues with map data due to the possiblity of jumps in the coordinate arrays. This adds a modulo wrap to the input arguments to do the interpolation in Cartopy before sending the arguments on to Matplotlib.
1 parent 8ca2e21 commit f70e61f

3 files changed

Lines changed: 103 additions & 12 deletions

File tree

lib/cartopy/mpl/geoaxes.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@
7070
CARTOPY_USER_BACKGROUNDS environment variable.
7171
"""
7272

73+
# A list of projections that can have wrapped coordinates.
74+
_WRAP_PROJECTIONS = (ccrs._RectangularProjection,
75+
ccrs._WarpedRectangularProjection,
76+
ccrs.InterruptedGoodeHomolosine,
77+
ccrs.Mercator,
78+
ccrs.LambertAzimuthalEqualArea,
79+
ccrs.AzimuthalEquidistant,
80+
ccrs.TransverseMercator,
81+
ccrs.Stereographic)
82+
7383

7484
# XXX call this InterCRSTransform
7585
class InterProjectionTransform(mtransforms.Transform):
@@ -1663,6 +1673,9 @@ def pcolormesh(self, *args, **kwargs):
16631673
A :class:`~cartopy.crs.Projection`.
16641674
16651675
"""
1676+
# Add in an argument checker to handle Matplotlib's potential
1677+
# interpolation when coordinate wraps are involved
1678+
args = self._wrap_args(*args, **kwargs)
16661679
result = matplotlib.axes.Axes.pcolormesh(self, *args, **kwargs)
16671680
# Wrap the quadrilaterals if necessary
16681681
result = self._wrap_quadmesh(result, **kwargs)
@@ -1673,22 +1686,62 @@ def pcolormesh(self, *args, **kwargs):
16731686
self.autoscale_view()
16741687
return result
16751688

1689+
def _wrap_args(self, *args, **kwargs):
1690+
"""
1691+
Handle the interpolation when a wrap could be involved with
1692+
the data coordinates before passing on to Matplotlib.
1693+
"""
1694+
if (kwargs.get('shading', 'auto') in ('nearest', 'auto') and
1695+
len(args) == 3 and
1696+
isinstance(kwargs.get('transform'), _WRAP_PROJECTIONS)):
1697+
X = np.asanyarray(args[0])
1698+
Y = np.asanyarray(args[1])
1699+
nrows, ncols = np.asanyarray(args[2]).shape
1700+
Nx = X.shape[-1]
1701+
Ny = Y.shape[0]
1702+
if X.ndim != 2 or X.shape[0] == 1:
1703+
x = X.reshape(1, Nx)
1704+
X = x.repeat(Ny, axis=0)
1705+
if Y.ndim != 2 or Y.shape[1] == 1:
1706+
y = Y.reshape(Ny, 1)
1707+
Y = y.repeat(Nx, axis=1)
1708+
1709+
def _interp_grid(X, wrap=0):
1710+
# helper for below
1711+
if np.shape(X)[1] > 1:
1712+
dX = np.diff(X, axis=1)
1713+
# account for the wrap
1714+
if wrap:
1715+
dX = (dX + wrap/2) % wrap - wrap/2
1716+
dX = dX/2
1717+
X = np.hstack((X[:, [0]] - dX[:, [0]],
1718+
X[:, :-1] + dX,
1719+
X[:, [-1]] + dX[:, [-1]]))
1720+
else:
1721+
# This is just degenerate, but we can't reliably guess
1722+
# a dX if there is just one value.
1723+
X = np.hstack((X, X))
1724+
return X
1725+
t = kwargs.get('transform')
1726+
xwrap = abs(t.x_limits[1] - t.x_limits[0])
1727+
if ncols == Nx:
1728+
X = _interp_grid(X, wrap=xwrap)
1729+
Y = _interp_grid(Y)
1730+
if nrows == Ny:
1731+
X = _interp_grid(X.T, wrap=xwrap).T
1732+
Y = _interp_grid(Y.T).T
1733+
1734+
args = (X, Y, args[2])
1735+
return args
1736+
16761737
def _wrap_quadmesh(self, collection, **kwargs):
16771738
"""
16781739
Handles the Quadmesh collection when any of the quadrilaterals
16791740
cross the boundary of the projection.
16801741
"""
16811742
t = kwargs.get('transform', None)
1682-
wrap_proj_types = (ccrs._RectangularProjection,
1683-
ccrs._WarpedRectangularProjection,
1684-
ccrs.InterruptedGoodeHomolosine,
1685-
ccrs.Mercator,
1686-
ccrs.LambertAzimuthalEqualArea,
1687-
ccrs.AzimuthalEquidistant,
1688-
ccrs.TransverseMercator,
1689-
ccrs.Stereographic)
1690-
if not (isinstance(t, wrap_proj_types) and
1691-
isinstance(self.projection, wrap_proj_types)):
1743+
if not (isinstance(t, _WRAP_PROJECTIONS) and
1744+
isinstance(self.projection, _WRAP_PROJECTIONS)):
16921745
# Nothing to do
16931746
return collection
16941747

@@ -1751,6 +1804,7 @@ def _wrap_quadmesh(self, collection, **kwargs):
17511804
# where the main plot is obscured
17521805
zorder = collection.zorder - .1
17531806
kwargs.pop('zorder', None)
1807+
kwargs.pop('shading', None)
17541808
kwargs.setdefault('snap', False)
17551809
vmin = kwargs.pop('vmin', None)
17561810
vmax = kwargs.pop('vmax', None)
@@ -1799,6 +1853,9 @@ def pcolor(self, *args, **kwargs):
17991853
A :class:`~cartopy.crs.Projection`.
18001854
18011855
"""
1856+
# Add in an argument checker to handle Matplotlib's potential
1857+
# interpolation when coordinate wraps are involved
1858+
args = self._wrap_args(*args, **kwargs)
18021859
result = matplotlib.axes.Axes.pcolor(self, *args, **kwargs)
18031860

18041861
# Update the datalim for this pcolor.

lib/cartopy/tests/mpl/test_mpl_integration.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,9 @@ def test_pcolormesh_single_column_wrap():
615615
plt.figure(figsize=(10, 6))
616616

617617
ax = plt.subplot(111, projection=ccrs.PlateCarree(180))
618-
plt.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral')
618+
# TODO: Remove snap when updating this image
619+
plt.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral',
620+
snap=False)
619621
ax.coastlines()
620622
ax.set_global()
621623

@@ -646,7 +648,8 @@ def test_pcolormesh_goode_wrap():
646648
Z = Z[:-1, :-1]
647649
ax = plt.axes(projection=ccrs.InterruptedGoodeHomolosine())
648650
ax.coastlines()
649-
ax.pcolormesh(x, y, Z, transform=ccrs.PlateCarree())
651+
# TODO: Remove snap when updating this image
652+
ax.pcolormesh(x, y, Z, transform=ccrs.PlateCarree(), snap=False)
650653

651654

652655
@pytest.mark.natural_earth

lib/cartopy/tests/mpl/test_pseudo_color.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,34 @@ def test_savefig_tight():
5353
buf = io.BytesIO()
5454
plt.savefig(buf, format='png', bbox_inches='tight')
5555
plt.close()
56+
57+
58+
def test_pcolormesh_arg_interpolation():
59+
# Test that making the edges from center point inputs
60+
# properly accounts for the wrapped coordinates appearing
61+
# in the middle of an array
62+
x = [359, 1, 3]
63+
y = [-10, 10]
64+
65+
xs, ys = np.meshgrid(x, y)
66+
# Z with the same shape as X/Y to force the interpolation
67+
z = np.zeros(xs.shape)
68+
69+
ax = plt.subplot(211, projection=ccrs.PlateCarree())
70+
coll = ax.pcolormesh(xs, ys, z, shading='auto',
71+
transform=ccrs.PlateCarree())
72+
73+
# Compare the output coordinates of the generated mesh
74+
expected = np.array([[[358, -20],
75+
[360, -20],
76+
[2, -20],
77+
[4, -20]],
78+
[[358, 0],
79+
[360, 0],
80+
[2, 0],
81+
[4, 0]],
82+
[[358, 20],
83+
[360, 20],
84+
[2, 20],
85+
[4, 20]]])
86+
np.testing.assert_array_almost_equal(expected, coll._coordinates)

0 commit comments

Comments
 (0)