Repository: SciTools/cartopy

Issue:
contourf and pcolormesh fail in some instances with ValueError: 'bboxes' cannot be empty
### Description
<!-- Please provide a general introduction to the issue/proposal. -->
I'm getting very strange behavior with matplotlib/cartopy. Yesterday, my code worked fine, but now today I get an error similar to what was discussed in #811 . I saw it proposed that just setting the map extent would prevent the issue, but that is not the case here. I thought I had figured out the solution to the problem by setting extend='both' in the contourf function, but it turns out that was not a long-term solution. Since the data I use changes every day, I'm not sure how reproducible this error is, but I will post the code here.  

It's also worth mentioning that I've had the same issue with pcolormesh at certain times; it worked fine until I set shading='gouraud', then it returned the error even though the plotting seemed fine. 

This is such a puzzling bug and I hope it can be fixed soon, else I will have to turn back to Basemap. 
<!--
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

```python
from datetime import datetime, timedelta
from urllib.request import urlopen
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
from metpy.units import masked_array, units
from netCDF4 import Dataset
import matplotlib
import cartopy.io.shapereader as shpreader

dt = datetime.utcnow()
url = 'http://water.weather.gov/precip/downloads/{dt:%Y/%m/%d}/nws_precip_1day_'\
      '{dt:%Y%m%d}_conus.nc'.format(dt=dt)
data = urlopen(url).read()
nc = Dataset('', memory=data)

prcpvar = nc.variables['observation']
data = masked_array(prcpvar[:], units(prcpvar.units.lower())).to('in')
x = nc.variables['x'][:]
y = nc.variables['y'][:]
proj_var = nc.variables[prcpvar.grid_mapping]

globe = ccrs.Globe(semimajor_axis=proj_var.earth_radius)
proj = ccrs.Stereographic(central_latitude=90.0,
                          central_longitude=proj_var.straight_vertical_longitude_from_pole,
                          true_scale_latitude=proj_var.standard_parallel, globe=globe)

# Mapping
mapproj = ccrs.Miller(central_longitude=-77)
fig = plt.figure(1,figsize=(15., 12.))
ax = fig.add_subplot(111, projection=mapproj)
#ax.set_extent([-85., -71, 37.2, 44.2], ccrs.PlateCarree()) # Midatl
ax.set_extent([-97, -90, 40.5, 44], ccrs.PlateCarree()) # Iowa
ax.coastlines('10m', edgecolor='black', linewidth=0.75)
states_provinces = cfeature.NaturalEarthFeature(
        category='cultural',
        name='admin_1_states_provinces_lakes',
        scale='10m',
        facecolor='none')
ax.add_feature(states_provinces, edgecolor='black', linewidth=0.5)
reader = shpreader.Reader('us_counties_5m.shp')
counties = list(reader.geometries())
COUNTIES = cfeature.ShapelyFeature(counties, ccrs.PlateCarree())
ax.add_feature(COUNTIES, facecolor='none', edgecolor='black', linewidth=0.3)

# Colormap
colors = [(1,1,1),(.75,.75,.75),(.65,.65,.65),(.5,.5,.5),(.43,.43,.43),(.7,1,.63),(.4,.95,.4),(.1,.7,.1), \
          (.06,.3,.55),(.04,.5,.67),(.02,.7,.8),(0,.9,.9),(.54,.4,.8),(.55,.2,.67),(.55,0,.55),(.55,0,0), \
          (.8,0,0),(.9,.25,0),(1,.5,0),(.8,.55,0),(.9,.78,0),(1,1,0),(1,.65,.75)]
cmap = matplotlib.colors.ListedColormap(colors)
cmap.set_over('white')
cmap.set_under('white')
bounds = [0, 0.01, 0.03, 0.05, 0.07, 0.1, 0.15, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 7, 10, 15, 20, 100]
labels = bounds[1:-1]
norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N)

# Plot
pcp = ax.contourf(x, y, data, cmap=cmap, levels=bounds, transform=proj, norm=norm, extend='both')

# Colorbar plotting
cb = plt.colorbar(pcp, cmap=cmap, pad=0.01, shrink=1, norm=norm, ticks=labels, orientation='horizontal', extendfrac=0)
cb.ax.set_aspect(.015) # Make it skinnier than the default
cb.outline.set_linewidth(1) # Set border width to match plot
cb.ax.set_xticklabels(cb.ax.get_xticklabels(), fontsize=10) # Tick label size

# Domain box
lonmax = -92.1
lonmin = -94.9
latmin = 42.3
latmax = 43.5
plt.plot([lonmax, lonmin], [latmin, latmin], color='red', linewidth=2, transform=ccrs.PlateCarree())
plt.plot([lonmax, lonmin], [latmax, latmax], color='red', linewidth=2, transform=ccrs.PlateCarree())
plt.plot([lonmax, lonmax], [latmin, latmax], color='red', linewidth=2, transform=ccrs.PlateCarree())
plt.plot([lonmin, lonmin], [latmax, latmin], color='red', linewidth=2, transform=ccrs.PlateCarree())

end = datetime.strptime(nc.creation_time[0:8], '%Y%m%d')
start = end - timedelta(days=1)
plt.title('AHPS Precipitation Analysis', loc='left', fontweight='bold', fontsize=14)
plt.title('Total Precip (in) Between '+start.strftime('12z %a %d %b')+' and '+end.strftime('12z %a %d %b %Y'), loc='right', fontsize=12)
plt.show()
```

#### Traceback 
```python-traceback
ValueError                                Traceback (most recent call last)
<ipython-input-25-1b7f31761f36> in <module>
     57 
     58 # Plot
---> 59 pcp = ax.contourf(x, y, data, cmap=cmap, levels=bounds, transform=proj, norm=norm, extend='both')
     60 
     61 # Colorbar plotting

~/anaconda3/lib/python3.7/site-packages/cartopy/mpl/geoaxes.py in contourf(self, *args, **kwargs)
   1402         # We need to compute the dataLim correctly for contours.
   1403         extent = mtransforms.Bbox.union([col.get_datalim(self.transData)
-> 1404                                          for col in result.collections
   1405                                          if col.get_paths()])
   1406         self.dataLim.update_from_data_xy(extent.get_points())

~/anaconda3/lib/python3.7/site-packages/matplotlib/transforms.py in union(bboxes)
    730         """
    731         if not len(bboxes):
--> 732             raise ValueError("'bboxes' cannot be empty")
    733         x0 = np.min([bbox.xmin for bbox in bboxes])
    734         x1 = np.max([bbox.xmax for bbox in bboxes])

ValueError: 'bboxes' cannot be empty
```
<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system

### Cartopy version
0.17.0

### conda list



### pip list

```
```

</details>



Discussion:
Update: So apparently removing extend='both' fixes my problem in this case. So my code now becomes: 
`try:`
`    pcp = ax.contourf(x, y, data, cmap=cmap, levels=bounds, transform=proj, norm=norm, extend='both')`
`except:`
`    pcp = ax.contourf(x, y, data, cmap=cmap, levels=bounds, transform=proj, norm=norm)`
Let's see how long this fix lasts for...
So I'm going to guess this is something specific to the dataset that is being downloaded. If I run your code as is (replacing your reading of counties with [`metpy.plots.USCOUNTIES`](https://unidata.github.io/MetPy/latest/examples/plots/US_Counties.html#sphx-glr-examples-plots-us-counties-py)), everything works fine. Can you find a file that fails, and then we can try to dig in from there?

I'm pretty sure what happens is that some of the data produces some weird contours (which is why `extend='both'` affected things). If we can reproduce, we can figure out how to fix it.
So I have done some more experimenting over the summer with various data sets, and have determined that the problem is not with the data. The error is not often reproducible because it depends on the colormap being used, not on the data itself. 

From what I can gather, this error is returned when none of the data within the map extent falls within the colormap or color levels being used. For example, if I try to plot an array of negative numbers and or NaN's using contour intervals that range from 0 to 100, for instance, it will result in this ValueError being raised. 

The workaround I've found is actually within my colormap generation. When I make my own colormaps, I also generate a norm object (either with matplotlib.colors.Normalize or BoundaryNorm). This normalizes the data to between 0-1 so contourf, pcolormesh, etc know what to do with it (pass the norm object to these matplotlib functions). By setting clip=True when I generate the norm object, it forces values outside of the colormap range to 0 or 1 depending on which side of the spectrum they fall on. This keeps the bboxes from ever being empy and suppresses the error. 

Maybe no one has seen this error before because most use the default matplotlib colormaps instead of making their own. In any case, I believe I have solved my own problem!

Before:
```python
cmap = mcolors.ListedColormap(self.colors, 'colormap')
norm = mcolors.BoundaryNorm(self.cbarLevels, cmap.N)
```

After:
```python
cmap = mcolors.ListedColormap(self.colors, 'colormap')
norm = mcolors.BoundaryNorm(self.cbarLevels, cmap.N, clip=True)
```
I occasionally run into this error when plotting data from the THREDDS server. This happened yesterday in the GFS CONUS 20km 2020-01-29 0600Z file. For the vorticity variable at the 10th forecast time step (2020-01-30 0900Z). 

There was data that was flagged as ```True``` in the masked array. 

![masked_array_true_2](https://user-images.githubusercontent.com/36716747/73377587-2ecf3000-42b7-11ea-989e-4115cc73506f.png)


And looking at the actual values in the dataset it appears as ```--```

![masked_array_true_1](https://user-images.githubusercontent.com/36716747/73377614-3abaf200-42b7-11ea-90be-09a157c7da41.png)

When the code got to this time step and tried to plot I got:

![masked_array_true_3](https://user-images.githubusercontent.com/36716747/73377810-91283080-42b7-11ea-94d9-95ab22913447.png)

As a side note, pages like College of DuPage must be using a different GFS dataset because their plot for the same time step and init hour had non-missing values...


![GFSUS_500_avort_027](https://user-images.githubusercontent.com/36716747/73379857-f2053800-42ba-11ea-855d-4cf6b0d2ffbf.png)



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
@@ -1494,10 +1494,12 @@ def contourf(self, *args, **kwargs):
         result = matplotlib.axes.Axes.contourf(self, *args, **kwargs)
 
         # We need to compute the dataLim correctly for contours.
-        extent = mtransforms.Bbox.union([col.get_datalim(self.transData)
-                                         for col in result.collections
-                                         if col.get_paths()])
-        self.dataLim.update_from_data_xy(extent.get_points())
+        bboxes = [col.get_datalim(self.transData)
+                  for col in result.collections
+                  if col.get_paths()]
+        if bboxes:
+            extent = mtransforms.Bbox.union(bboxes)
+            self.dataLim.update_from_data_xy(extent.get_points())
 
         self.autoscale_view()
 


Test patch:
diff --git a/lib/cartopy/tests/mpl/test_contour.py b/lib/cartopy/tests/mpl/test_contour.py
--- a/lib/cartopy/tests/mpl/test_contour.py
+++ b/lib/cartopy/tests/mpl/test_contour.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2016 - 2018, Met Office
+# (C) British Crown Copyright 2016 - 2020, Met Office
 #
 # This file is part of cartopy.
 #
@@ -41,6 +41,11 @@ def test_contour_plot_bounds():
     assert_array_almost_equal(ax.get_extent(),
                               np.array([x[0], x[-1], y[0], y[-1]]))
 
+    # Levels that don't include data should not fail.
+    plt.figure()
+    ax = plt.axes(projection=proj_lcc)
+    ax.contourf(x, y, data, levels=np.max(data) + np.arange(1, 3))
+
 
 @cleanup
 def test_contour_linear_ring():