Skip to content

Commit a8badb6

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 0b5db24 commit a8badb6

2 files changed

Lines changed: 97 additions & 10 deletions

File tree

lib/cartopy/mpl/geoaxes.py

Lines changed: 66 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):
@@ -1649,6 +1659,9 @@ def pcolormesh(self, *args, **kwargs):
16491659
A :class:`~cartopy.crs.Projection`.
16501660
16511661
"""
1662+
# Add in an argument checker to handle Matplotlib's potential
1663+
# interpolation when coordinate wraps are involved
1664+
args = self._wrap_args(*args, **kwargs)
16521665
result = matplotlib.axes.Axes.pcolormesh(self, *args, **kwargs)
16531666
# Wrap the quadrilaterals if necessary
16541667
result = self._wrap_quadmesh(result, **kwargs)
@@ -1659,22 +1672,62 @@ def pcolormesh(self, *args, **kwargs):
16591672
self.autoscale_view()
16601673
return result
16611674

1675+
def _wrap_args(self, *args, **kwargs):
1676+
"""
1677+
Handle the interpolation when a wrap could be involved with
1678+
the data coordinates before passing on to Matplotlib.
1679+
"""
1680+
if (kwargs.get('shading', 'auto') in ('nearest', 'auto') and
1681+
len(args) == 3 and
1682+
isinstance(kwargs.get('transform'), _WRAP_PROJECTIONS)):
1683+
X = np.asanyarray(args[0])
1684+
Y = np.asanyarray(args[1])
1685+
nrows, ncols = np.asanyarray(args[2]).shape
1686+
Nx = X.shape[-1]
1687+
Ny = Y.shape[0]
1688+
if X.ndim != 2 or X.shape[0] == 1:
1689+
x = X.reshape(1, Nx)
1690+
X = x.repeat(Ny, axis=0)
1691+
if Y.ndim != 2 or Y.shape[1] == 1:
1692+
y = Y.reshape(Ny, 1)
1693+
Y = y.repeat(Nx, axis=1)
1694+
1695+
def _interp_grid(X, wrap=0):
1696+
# helper for below
1697+
if np.shape(X)[1] > 1:
1698+
dX = np.diff(X, axis=1)
1699+
# account for the wrap
1700+
if wrap:
1701+
dX = (dX + wrap/2) % wrap - wrap/2
1702+
dX = dX/2
1703+
X = np.hstack((X[:, [0]] - dX[:, [0]],
1704+
X[:, :-1] + dX,
1705+
X[:, [-1]] + dX[:, [-1]]))
1706+
else:
1707+
# This is just degenerate, but we can't reliably guess
1708+
# a dX if there is just one value.
1709+
X = np.hstack((X, X))
1710+
return X
1711+
t = kwargs.get('transform')
1712+
xwrap = abs(t.x_limits[1] - t.x_limits[0])
1713+
if ncols == Nx:
1714+
X = _interp_grid(X, wrap=xwrap)
1715+
Y = _interp_grid(Y)
1716+
if nrows == Ny:
1717+
X = _interp_grid(X.T, wrap=xwrap).T
1718+
Y = _interp_grid(Y.T).T
1719+
1720+
args = (X, Y, args[2])
1721+
return args
1722+
16621723
def _wrap_quadmesh(self, collection, **kwargs):
16631724
"""
16641725
Handles the Quadmesh collection when any of the quadrilaterals
16651726
cross the boundary of the projection.
16661727
"""
16671728
t = kwargs.get('transform', None)
1668-
wrap_proj_types = (ccrs._RectangularProjection,
1669-
ccrs._WarpedRectangularProjection,
1670-
ccrs.InterruptedGoodeHomolosine,
1671-
ccrs.Mercator,
1672-
ccrs.LambertAzimuthalEqualArea,
1673-
ccrs.AzimuthalEquidistant,
1674-
ccrs.TransverseMercator,
1675-
ccrs.Stereographic)
1676-
if not (isinstance(t, wrap_proj_types) and
1677-
isinstance(self.projection, wrap_proj_types)):
1729+
if not (isinstance(t, _WRAP_PROJECTIONS) and
1730+
isinstance(self.projection, _WRAP_PROJECTIONS)):
16781731
# Nothing to do
16791732
return collection
16801733

@@ -1785,6 +1838,9 @@ def pcolor(self, *args, **kwargs):
17851838
A :class:`~cartopy.crs.Projection`.
17861839
17871840
"""
1841+
# Add in an argument checker to handle Matplotlib's potential
1842+
# interpolation when coordinate wraps are involved
1843+
args = self._wrap_args(*args, **kwargs)
17881844
result = matplotlib.axes.Axes.pcolor(self, *args, **kwargs)
17891845

17901846
# Update the datalim for this pcolor.

lib/cartopy/tests/mpl/test_pseudo_color.py

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

0 commit comments

Comments
 (0)