Repository: SciTools/cartopy

Issue:
cartopy 0.22 + matplotlib 3.8 + contourf bug?
### Description
After upgrading to Matplotlib3.8, it is suspected that there is a bug in cartopy 0.22 when drawing contourfs. Test code and draw results in [test code and draw result](https://github.com/david-shu/cartopy-contourf-data)

The problem seems to be here. when not force_path_ccw, got the right contourf.
```
class GeoAxes(matplotlib.axes.Axes):
    def contourf(self, *args, **kwargs):
            ##....................
            if isinstance(t, mtransforms.Transform):
                for sub_trans, _ in t._iter_break_from_left_to_right():
                    if isinstance(sub_trans, InterProjectionTransform):
                        if not hasattr(sub_trans, 'force_path_ccw'):
                            sub_trans.force_path_ccw = True
           ##.......................
    
```

The same code is drawn correctly on Matplotlib3.7.


the same data. but got different contourf. see

![contourf result](https://github.com/david-shu/cartopy-contourf-data/blob/main/contourf.png)


#### Code to reproduce

```
import PyQt6.QtCore
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
from urllib.request import urlopen


proj0 = ccrs.PlateCarree( central_longitude=0 )

x = np.load( urlopen( 'https://github.com/david-shu/cartopy-contourf-data/raw/main/x.dat' ) )
y = np.load( urlopen( 'https://github.com/david-shu/cartopy-contourf-data/raw/main/y.dat' ) )
v = np.load( urlopen( 'https://github.com/david-shu/cartopy-contourf-data/raw/main/v.dat' ) )

shape = ( 61,240 )

x = x.reshape( shape )
y = y.reshape( shape )
v = v.reshape( shape )

levels = [-4.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 4.0]

### contourf wrong ###### 
ax = plt.subplot( 2, 2, 1, projection=proj0 )
cs = ax.contourf( x, y, v,
                  levels=levels,
                  extend='both',
                  transform_first=False
                  )
plt.colorbar( cs )
ax.set_title('PlateCarree + extend both' )
###########################


ax = plt.subplot( 2, 2, 2, projection=proj0 )
cs = ax.contourf( x, y, v,
                  levels=levels,
                  extend='min',
                  transform_first=False
                  )
plt.colorbar( cs )
ax.set_title('PlateCarree + extend min' )


ax = plt.subplot( 2, 2,3, projection=proj0 )
cs = ax.contourf( x, y, v,
                  levels=levels,
                  extend='both',
                  transform_first=True
                  )
plt.colorbar( cs )
ax.set_title('PlateCarree + transform_first' )

ax = plt.subplot( 2, 2, 4 )
cs = ax.contourf( x, y, v,
                  levels=levels,
                  extend='both',
                  )
plt.colorbar( cs )
ax.set_title('Axes' )

plt.show()
```



<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system
win10 / centos

### Cartopy version
cartopy 0.22.0
cartopy 0.21.1

### matplotlib version
matplotlib 3.8.1
</details>



Discussion:
If we remove all of the `force_ccw` stuff from Cartopy all of the tests still pass, so maybe Matplotlib was fixed upstream and that block of code is no longer needed in Cartopy?

https://github.com/SciTools/cartopy/issues/253
The fix came in: https://github.com/SciTools/cartopy/pull/280
but with no unit test and the original issue data is now no longer available.

Reading through that issue, it looks like this was identified as a bug in Matplotlib originally but they tried to work around it in Cartopy. The contouring work in Matplotlib has changed quite a bit since then and now the contouring is handled in contourpy. My guess is this is a relic of the past and fixed somewhere along the line and might be able to be removed altogether now.
I've been doing some digging.  For the OP's example, _all_ the geometries show as clockwise in this loop, so get their direction flipped.

https://github.com/SciTools/cartopy/blob/b363e437a77be344d85ce8d56a8537f218b4ec8d/lib/cartopy/mpl/patch.py#L201-L204

This is also true at Matplotlib v3.7, except we ended up in this loop less often: since mpl 3.7's ContourSet had multiple paths instead of one big path for each level, a lot of them get processed through the "quick transform", so never get converted to shapely objects.  If we comment out the "quick transform" code and make everything go through shapely, then mpl 3.7 gives a completely yellow plot for the problem subplot.

https://github.com/SciTools/cartopy/blob/b363e437a77be344d85ce8d56a8537f218b4ec8d/lib/cartopy/mpl/geoaxes.py#L162-L168

I wondered if the fact that the OP's latitudes run north-south was a factor in the contours all being clockwise, so I tried

```python
y = np.flip(y.reshape( shape ), axis=0)
v = np.flip(v.reshape( shape ), axis=0)
```
This leads to most of the geometries being automatically counter-clockwise, but the bug still shows.  The bug also still shows if I comment out the line that flips the direction of the remaining clockwise geometries.  So now I am just very confused...
I also see there was some doubt at https://github.com/SciTools/cartopy/pull/280#issuecomment-21338002 over whether the original problem was really fixed.

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
@@ -171,12 +171,7 @@ def transform_path_non_affine(self, src_path):
             return mpath.Path(self.transform(src_path.vertices))
 
         transformed_geoms = []
-        # Check whether this transform has the "force_path_ccw" attribute set.
-        # This is a cartopy extension to the Transform API to allow finer
-        # control of Path orientation handling (Path ordering is not important
-        # in matplotlib, but is in Cartopy).
-        geoms = cpatch.path_to_geos(src_path,
-                                    getattr(self, 'force_path_ccw', False))
+        geoms = cpatch.path_to_geos(src_path)
 
         for geom in geoms:
             proj_geom = self.target_projection.project_geometry(
@@ -1639,16 +1634,6 @@ def contourf(self, *args, **kwargs):
             arguments X and Y must be provided and be 2-dimensional.
             The default is False, to compute the contours in data-space.
         """
-        t = kwargs.get('transform')
-        if isinstance(t, ccrs.Projection):
-            kwargs['transform'] = t = t._as_mpl_transform(self)
-        # Set flag to indicate correcting orientation of paths if not ccw
-        if isinstance(t, mtransforms.Transform):
-            for sub_trans, _ in t._iter_break_from_left_to_right():
-                if isinstance(sub_trans, InterProjectionTransform):
-                    if not hasattr(sub_trans, 'force_path_ccw'):
-                        sub_trans.force_path_ccw = True
-
         result = super().contourf(*args, **kwargs)
 
         # We need to compute the dataLim correctly for contours.


Test patch: