Repository: SciTools/cartopy

Issue:
contourf, pcolormesh plots disappear in combination with set_boundary
### Description
<!-- Please provide a general introduction to the issue/proposal. -->
When using either `contourf` or `pcolormesh`, the field being plotted disappears after calling `set_boundary`.

<!--
If you are reporting a bug, attach the *entire* traceback from Python.

If you are proposing an enhancement/new feature, provide links to related articles, reference examples, etc.

If you are asking a question, please ask on StackOverflow and use the cartopy tag. All cartopy
questions on StackOverflow can be found at https://stackoverflow.com/questions/tagged/cartopy
-->

#### Code to reproduce

Here is a slight modification of `always_circular_stereo.py` to demonstrate the issue:
```
#!/usr/bin/env python

import matplotlib.path as mpath
import matplotlib.pyplot as plt
import numpy as np

import cartopy.crs as ccrs
import cartopy.feature as cfeature


def main():
    lon = np.linspace(-180., 180., 361)
    lat = np.linspace(-90., -60., 31)
    Lon, Lat = np.meshgrid(lon, lat)
    fig = plt.figure(figsize=[10, 5])
    ax1 = fig.add_subplot(1, 2, 1, projection=ccrs.SouthPolarStereo())
    ax2 = fig.add_subplot(1, 2, 2, projection=ccrs.SouthPolarStereo(),
                          sharex=ax1, sharey=ax1)
    fig.subplots_adjust(bottom=0.05, top=0.95,
                        left=0.04, right=0.95, wspace=0.02)

    # Limit the map to -60 degrees latitude and below.
    ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())
    ax2.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())

    ax1.add_feature(cfeature.LAND)
    ax1.add_feature(cfeature.OCEAN)

    ax1.gridlines()
    ax2.gridlines()

    ax2.add_feature(cfeature.LAND)
    ax2.add_feature(cfeature.OCEAN)
    
    p1 = ax1.contourf(lon, lat, Lat, transform=ccrs.PlateCarree())
    p2 = ax2.contourf(lon, lat, Lat, transform=ccrs.PlateCarree())

    # Compute a circle in axes coordinates, which we can use as a boundary
    # for the map. We can pan/zoom as much as we like - the boundary will be
    # permanently circular.
    theta = np.linspace(0, 2*np.pi, 100)
    center, radius = [0.5, 0.5], 0.5
    verts = np.vstack([np.sin(theta), np.cos(theta)]).T
    circle = mpath.Path(verts * radius + center)

    ax2.set_boundary(circle, transform=ax2.transAxes)

    plt.show()


if __name__ == '__main__':
    main()
```
In cartopy 0.17.0, the result is:
![cartopy_0 17 0](https://user-images.githubusercontent.com/4179064/88938479-b8bb7e00-d285-11ea-9ebe-626f2da57794.png)

However, in cartopy 0.18.0, the contourf plot disappears:
![cartopy_0 18 0](https://user-images.githubusercontent.com/4179064/88938556-cb35b780-d285-11ea-9c42-66638e82a049.png)

As a side note, the behavior in cartopy 0.17.0 was still unexpected.  If the field was not already limited to or masked to the region contained within the circle, it did not get cropped to the boundary:
![cartopy_0 17 0_extended](https://user-images.githubusercontent.com/4179064/88938746-fddfb000-d285-11ea-9d07-34fbb91d8cfd.png)

I have tried this with a `ccrs.PlateCarree()` plot, so it doesn't seem to be specific to `ccrs.SouthPolarStereo()`.

<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system
Ubuntu 18.04.4

### Cartopy version
cartopy 0.18.0

### conda list

```
$ conda list
# packages in environment at /home/xylar/miniconda3/envs/test:
#
# Name                    Version                   Build  Channel
_libgcc_mutex             0.1                 conda_forge    conda-forge
_openmp_mutex             4.5                       0_gnu    conda-forge
boost-cpp                 1.72.0               h7b93d67_2    conda-forge
brotlipy                  0.7.0           py37h8f50634_1000    conda-forge
bzip2                     1.0.8                h516909a_2    conda-forge
c-ares                    1.16.1               h516909a_0    conda-forge
ca-certificates           2020.6.20            hecda079_0    conda-forge
cairo                     1.16.0            h3fc0475_1005    conda-forge
cartopy                   0.18.0           py37h55f60b2_1    conda-forge
certifi                   2020.6.20        py37hc8dfbb8_0    conda-forge
cffi                      1.14.1           py37h2b28604_0    conda-forge
cfitsio                   3.470                hce51eda_6    conda-forge
chardet                   3.0.4           py37hc8dfbb8_1006    conda-forge
cryptography              3.0              py37hb09aad4_0    conda-forge
curl                      7.71.1               he644dc0_4    conda-forge
cycler                    0.10.0                     py_2    conda-forge
expat                     2.2.9                he1b5a44_2    conda-forge
fontconfig                2.13.1            h1056068_1002    conda-forge
freetype                  2.10.2               he06d7ca_0    conda-forge
freexl                    1.0.5             h516909a_1002    conda-forge
gdal                      3.1.2            py37h518339e_1    conda-forge
geos                      3.8.1                he1b5a44_0    conda-forge
geotiff                   1.6.0                ha04d9d0_1    conda-forge
gettext                   0.19.8.1          hc5be6a0_1002    conda-forge
giflib                    5.2.1                h516909a_2    conda-forge
glib                      2.65.0               h6f030ca_0    conda-forge
hdf4                      4.2.13            hf30be14_1003    conda-forge
hdf5                      1.10.6          nompi_h3c11f04_100    conda-forge
icu                       67.1                 he1b5a44_0    conda-forge
idna                      2.10               pyh9f0ad1d_0    conda-forge
jpeg                      9d                   h516909a_0    conda-forge
json-c                    0.13.1            hbfbb72e_1002    conda-forge
kealib                    1.4.13               h33137a7_1    conda-forge
kiwisolver                1.2.0            py37h99015e2_0    conda-forge
krb5                      1.17.1               hfafb76e_2    conda-forge
lcms2                     2.11                 hbd6801e_0    conda-forge
ld_impl_linux-64          2.34                 hc38a660_9    conda-forge
libblas                   3.8.0               17_openblas    conda-forge
libcblas                  3.8.0               17_openblas    conda-forge
libcurl                   7.71.1               hcdd3856_4    conda-forge
libdap4                   3.20.6               h1d1bd15_1    conda-forge
libedit                   3.1.20191231         h46ee950_1    conda-forge
libev                     4.33                 h516909a_0    conda-forge
libffi                    3.2.1             he1b5a44_1007    conda-forge
libgcc-ng                 9.3.0               h24d8f2e_11    conda-forge
libgdal                   3.1.2                hb2a6f5f_1    conda-forge
libgfortran-ng            7.5.0               hdf63c60_11    conda-forge
libgomp                   9.3.0               h24d8f2e_11    conda-forge
libiconv                  1.15              h516909a_1006    conda-forge
libkml                    1.3.0             hb574062_1011    conda-forge
liblapack                 3.8.0               17_openblas    conda-forge
libnetcdf                 4.7.4           nompi_h84807e1_105    conda-forge
libnghttp2                1.41.0               hab1572f_1    conda-forge
libopenblas               0.3.10          pthreads_hb3c22a3_4    conda-forge
libpng                    1.6.37               hed695b0_1    conda-forge
libpq                     12.3                 h5513abc_0    conda-forge
libspatialite             4.3.0a            h57f1b35_1039    conda-forge
libssh2                   1.9.0                hab1572f_5    conda-forge
libstdcxx-ng              9.3.0               hdf63c60_11    conda-forge
libtiff                   4.1.0                hc7e4089_6    conda-forge
libuuid                   2.32.1            h14c3975_1000    conda-forge
libwebp-base              1.1.0                h516909a_3    conda-forge
libxcb                    1.13              h14c3975_1002    conda-forge
libxml2                   2.9.10               h72b56ed_2    conda-forge
lz4-c                     1.9.2                he1b5a44_1    conda-forge
matplotlib                3.3.0                         1    conda-forge
matplotlib-base           3.3.0            py37hd478181_1    conda-forge
ncurses                   6.2                  he1b5a44_1    conda-forge
numpy                     1.19.1           py37h8960a57_0    conda-forge
olefile                   0.46                       py_0    conda-forge
openjpeg                  2.3.1                h981e76c_3    conda-forge
openssl                   1.1.1g               h516909a_1    conda-forge
owslib                    0.20.0                     py_0    conda-forge
pcre                      8.44                 he1b5a44_0    conda-forge
pillow                    7.2.0            py37h718be6c_1    conda-forge
pip                       20.2                       py_0    conda-forge
pixman                    0.38.0            h516909a_1003    conda-forge
poppler                   0.89.0               h4190859_1    conda-forge
poppler-data              0.4.9                         1    conda-forge
postgresql                12.3                 h8573dbc_0    conda-forge
proj                      7.1.0                h966b41f_1    conda-forge
pthread-stubs             0.4               h14c3975_1001    conda-forge
pycparser                 2.20               pyh9f0ad1d_2    conda-forge
pyepsg                    0.4.0                      py_0    conda-forge
pyopenssl                 19.1.0                     py_1    conda-forge
pyparsing                 2.4.7              pyh9f0ad1d_0    conda-forge
pyproj                    2.6.1.post1      py37hb5dadc3_1    conda-forge
pyshp                     2.1.0                      py_0    conda-forge
pysocks                   1.7.1            py37hc8dfbb8_1    conda-forge
python                    3.7.8           h6f2ec95_0_cpython    conda-forge
python-dateutil           2.8.1                      py_0    conda-forge
python_abi                3.7                     1_cp37m    conda-forge
pytz                      2020.1             pyh9f0ad1d_0    conda-forge
pyyaml                    5.3.1            py37h8f50634_0    conda-forge
readline                  8.0                  he28a2e2_2    conda-forge
requests                  2.24.0             pyh9f0ad1d_0    conda-forge
scipy                     1.5.2            py37hb14ef9d_0    conda-forge
setuptools                49.2.0           py37hc8dfbb8_0    conda-forge
shapely                   1.7.0            py37hc88ce51_3    conda-forge
six                       1.15.0             pyh9f0ad1d_0    conda-forge
sqlite                    3.32.3               hcee41ef_1    conda-forge
tbb                       2020.1               hc9558a2_0    conda-forge
tiledb                    2.0.7                h3effe38_0    conda-forge
tk                        8.6.10               hed695b0_0    conda-forge
tornado                   6.0.4            py37h8f50634_1    conda-forge
tzcode                    2020a                h516909a_0    conda-forge
urllib3                   1.25.10                    py_0    conda-forge
wheel                     0.34.2                     py_1    conda-forge
xerces-c                  3.2.3                hfe33f54_1    conda-forge
xorg-kbproto              1.0.7             h14c3975_1002    conda-forge
xorg-libice               1.0.10               h516909a_0    conda-forge
xorg-libsm                1.2.3             h84519dc_1000    conda-forge
xorg-libx11               1.6.9                h516909a_0    conda-forge
xorg-libxau               1.0.9                h14c3975_0    conda-forge
xorg-libxdmcp             1.1.3                h516909a_0    conda-forge
xorg-libxext              1.3.4                h516909a_0    conda-forge
xorg-libxrender           0.9.10            h516909a_1002    conda-forge
xorg-renderproto          0.11.1            h14c3975_1002    conda-forge
xorg-xextproto            7.3.0             h14c3975_1002    conda-forge
xorg-xproto               7.0.31            h14c3975_1007    conda-forge
xz                        5.2.5                h516909a_1    conda-forge
yaml                      0.2.5                h516909a_0    conda-forge
zlib                      1.2.11            h516909a_1006    conda-forge
zstd                      1.4.5                h6597ccf_2    conda-forge
```
</details>



Discussion:
@chengzhuzhang, please feel fee to comment if you have anything you want to add.
I'm not sure what is actually going on here, but likely something with the clipping using the new path. (Maybe a transform issue in the stack somewhere?)

For a workaround now you can reorder the calls and `set_boundary()` before you start with any plotting calls.
```python
    ax2.set_boundary(circle, transform=ax2.transAxes)
    p1 = ax1.contourf(lon, lat, Lat, transform=ccrs.PlateCarree())
    p2 = ax2.contourf(lon, lat, Lat, transform=ccrs.PlateCarree())
```
Thanks @greglucas! @chengzhuzhang, can you see if this workaround works for you?
@xylar I just confirmed reordering and  using `set_boundary()` before the plotting call works in our use case. Thank you @greglucas !
Glad you are able to use that workaround! Let's keep this open so we can try and fix it properly still.

Some notes:
- when panning around the second axis, the first axis moves around with it due to the sharex/sharey, which is not right
- if the center (0, -90) leaves the circular boundary the entire figure gets the plot indicating the axes clipping no longer works. When doing that without the workaround, the data also then shows up.
Ugh, it's a subtle issue with transforms not properly being updated. PR incoming...

Resolving patch:
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -235,14 +235,29 @@ def __init__(self, axes, **kwargs):
         super().__init__(self._original_path, **kwargs)
         self._axes = axes
 
+        # We need to use a TransformWrapper as our transform so that we can
+        # update the transform without breaking others' references to this one.
+        self._trans_wrap = mtransforms.TransformWrapper(self.get_transform())
+
+    def set_transform(self, transform):
+        self._trans_wrap.set(transform)
+        super().set_transform(self._trans_wrap)
+
     def set_boundary(self, path, transform):
         self._original_path = path
         self.set_transform(transform)
         self.stale = True
 
+    # Can remove and use matplotlib's once we support only >= 3.2
+    def set_path(self, path):
+        self._path = path
+
     def _adjust_location(self):
         if self.stale:
-            self._path = self._original_path.clip_to_bbox(self.axes.viewLim)
+            self.set_path(self._original_path.clip_to_bbox(self.axes.viewLim))
+            # Some places in matplotlib's tranform stack cache the actual
+            # path so we trigger an update by invalidating the transform.
+            self._trans_wrap.invalidate()
 
     @matplotlib.artist.allow_rasterization
     def draw(self, renderer, *args, **kwargs):


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png
new file mode 100644
Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png differ
diff --git a/lib/cartopy/tests/mpl/test_axes.py b/lib/cartopy/tests/mpl/test_axes.py
--- a/lib/cartopy/tests/mpl/test_axes.py
+++ b/lib/cartopy/tests/mpl/test_axes.py
@@ -132,3 +132,21 @@ def test_geoaxes_no_subslice():
         lats = np.linspace(35, 37, num_points)
         lons = np.linspace(-117, -115, num_points)
         ax.plot(lons, lats, transform=ccrs.PlateCarree())
+
+
+@ImageTesting(['geoaxes_set_boundary_clipping'])
+def test_geoaxes_set_boundary_clipping():
+    """Test that setting the boundary works properly for clipping #1620."""
+    lon, lat = np.meshgrid(np.linspace(-180., 180., 361),
+                           np.linspace(-90., -60., 31))
+    fig = plt.figure()
+    ax1 = fig.add_subplot(1, 1, 1, projection=ccrs.SouthPolarStereo())
+
+    # Limit the map to -60 degrees latitude and below.
+    ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())
+    ax1.gridlines()
+
+    ax1.contourf(lon, lat, lat, transform=ccrs.PlateCarree())
+
+    ax1.set_boundary(mpath.Path.circle(center=(0.5, 0.5), radius=0.5),
+                     transform=ax1.transAxes)