Repository: geopandas/geopandas

Issue:
REGR: plot() no longer working if empty geometries are present
(unfortunately discovered while preparing my course last week .. ;))

It seems that plotting with empty geometries no longer works with 0.8, while this certainly worked before, at least for polygons (following code examples are using geopandas master):

```
import geopandas
from shapely.geometry import Point, Polygon
from shapely import wkt
```

Without using pygeos:

```python
In [16]: geopandas.options.use_pygeos = False

# empty "polygon" which is actually an empty GeometryCollection
In [19]: s = geopandas.GeoSeries([Polygon([(0, 0), (1, 0), (1, 1)]), Polygon()])

In [20]: s.plot()
...
~/scipy/geopandas/geopandas/plotting.py in _flatten_multi_geoms(geoms, prefix)
     47     for ix, geom in enumerate(geoms):
     48         if geom.type.startswith(prefix):
---> 49             for poly in geom:
     50                 components.append(poly)
     51                 component_index.append(ix)

TypeError: 'Polygon' object is not iterable

# with a real empty polygon
In [21]: s = geopandas.GeoSeries([Polygon([(0, 0), (1, 0), (1, 1)]), wkt.loads("POLYGON EMPTY")])

In [22]: s.plot()
...
~/scipy/geopandas/geopandas/plotting.py in plot_series(s, cmap, color, ax, figsize, aspect, **style_kwds)
    411 
    412         values_ = values[poly_idx] if cmap else None
--> 413         _plot_polygon_collection(
    414             ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds
    415         )

~/scipy/geopandas/geopandas/plotting.py in _plot_polygon_collection(ax, geoms, values, color, cmap, vmin, vmax, **kwargs)
    144     _expand_kwargs(kwargs, multiindex)
    145 
--> 146     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
    147 
    148     if values is not None:

~/scipy/geopandas/geopandas/plotting.py in <listcomp>(.0)
    144     _expand_kwargs(kwargs, multiindex)
    145 
--> 146     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
    147 
    148     if values is not None:

~/miniconda3/envs/geo-dev/lib/python3.8/site-packages/descartes/patch.py in PolygonPatch(polygon, **kwargs)
     85 
     86     """
---> 87     return PathPatch(PolygonPath(polygon), **kwargs)

~/miniconda3/envs/geo-dev/lib/python3.8/site-packages/descartes/patch.py in PolygonPath(polygon)
     60                 "A polygon or multi-polygon representation is required")
     61 
---> 62     vertices = concatenate([
     63         concatenate([asarray(t.exterior)[:, :2]] +
     64                     [asarray(r)[:, :2] for r in t.interiors])

~/miniconda3/envs/geo-dev/lib/python3.8/site-packages/descartes/patch.py in <listcomp>(.0)
     61 
     62     vertices = concatenate([
---> 63         concatenate([asarray(t.exterior)[:, :2]] +
     64                     [asarray(r)[:, :2] for r in t.interiors])
     65         for t in polygon])

~/miniconda3/envs/geo-dev/lib/python3.8/site-packages/descartes/patch.py in exterior(self)
     17     def exterior(self):
     18         return (getattr(self.context, 'exterior', None)
---> 19                 or self.context[0])
     20 
     21     @property

TypeError: 'Polygon' object is not subscriptable
```

when using pygeos, a few more cases work:

```python
In [6]: geopandas.options.use_pygeos = True

# empty GeometryCollection works
In [12]: s = geopandas.GeoSeries([Polygon([(0, 0), (1, 0), (1, 1)]), Polygon()])

In [13]: s.plot()
Out[13]: <AxesSubplot:>

# but real empty polygon still fails with the same error as above
In [14]: s = geopandas.GeoSeries([Polygon([(0, 0), (1, 0), (1, 1)]), wkt.loads("POLYGON EMPTY")])

In [15]: s.plot()
....
TypeError: 'Polygon' object is not subscriptable
```

Comments:

Patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 25a62e9..0257919 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -84,9 +84,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
 
     _geometry_column_name = DEFAULT_GEO_COLUMN_NAME
 
-    def __init__(self, *args, **kwargs):
-        crs = kwargs.pop("crs", None)
-        geometry = kwargs.pop("geometry", None)
+    def __init__(self, *args, geometry=None, crs=None, **kwargs):
         with compat.ignore_shapely2_warnings():
             super(GeoDataFrame, self).__init__(*args, **kwargs)
 
@@ -1188,7 +1186,8 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
         Parameters
         ----------
         by : string, default None
-            Column whose values define groups to be dissolved
+            Column whose values define groups to be dissolved. If None,
+            whole GeoDataFrame is considered a single group.
         aggfunc : function or string, default "first"
             Aggregation function for manipulation of data associated
             with each group. Passed to pandas `groupby.agg` method.
@@ -1222,6 +1221,9 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
 
         """
 
+        if by is None:
+            by = np.zeros(len(self), dtype="int64")
+
         # Process non-spatial component
         data = self.drop(labels=self.geometry.name, axis=1)
         aggregated_data = data.groupby(by=by).agg(aggfunc)
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index e84dcfb..b4df187 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -45,7 +45,7 @@ def _flatten_multi_geoms(geoms, prefix="Multi"):
         return geoms, np.arange(len(geoms))
 
     for ix, geom in enumerate(geoms):
-        if geom.type.startswith(prefix):
+        if geom.type.startswith(prefix) and not geom.is_empty:
             for poly in geom.geoms:
                 components.append(poly)
                 component_index.append(ix)
@@ -151,7 +151,9 @@ def _plot_polygon_collection(
 
     _expand_kwargs(kwargs, multiindex)
 
-    collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
+    collection = PatchCollection(
+        [PolygonPatch(poly) for poly in geoms if not poly.is_empty], **kwargs
+    )
 
     if values is not None:
         collection.set_array(np.asarray(values))
@@ -385,6 +387,14 @@ def plot_series(
         )
         return ax
 
+    if s.is_empty.all():
+        warnings.warn(
+            "The GeoSeries you are attempting to plot is "
+            "composed of empty geometries. Nothing has been displayed.",
+            UserWarning,
+        )
+        return ax
+
     # if cmap is specified, create range of colors based on cmap
     values = None
     if cmap is not None:


Test patch:
diff --git a/geopandas/tests/test_dissolve.py b/geopandas/tests/test_dissolve.py
index d2485ed..cc5c6ee 100644
--- a/geopandas/tests/test_dissolve.py
+++ b/geopandas/tests/test_dissolve.py
@@ -99,3 +99,32 @@ def test_reset_index(nybb_polydf, first):
     test = nybb_polydf.dissolve("manhattan_bronx", as_index=False)
     comparison = first.reset_index()
     assert_frame_equal(comparison, test, check_column_type=False)
+
+
+def test_dissolve_none(nybb_polydf):
+    test = nybb_polydf.dissolve(by=None)
+    expected = GeoDataFrame(
+        {
+            nybb_polydf.geometry.name: [nybb_polydf.geometry.unary_union],
+            "BoroName": ["Staten Island"],
+            "BoroCode": [5],
+            "manhattan_bronx": [5],
+        },
+        geometry=nybb_polydf.geometry.name,
+        crs=nybb_polydf.crs,
+    )
+    assert_frame_equal(expected, test, check_column_type=False)
+
+
+def test_dissolve_none_mean(nybb_polydf):
+    test = nybb_polydf.dissolve(aggfunc="mean")
+    expected = GeoDataFrame(
+        {
+            nybb_polydf.geometry.name: [nybb_polydf.geometry.unary_union],
+            "BoroCode": [3.0],
+            "manhattan_bronx": [5.4],
+        },
+        geometry=nybb_polydf.geometry.name,
+        crs=nybb_polydf.crs,
+    )
+    assert_frame_equal(expected, test, check_column_type=False)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 60c307e..6653af3 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -4,6 +4,7 @@ import warnings
 import numpy as np
 import pandas as pd
 
+from shapely import wkt
 from shapely.affinity import rotate
 from shapely.geometry import (
     MultiPolygon,
@@ -19,6 +20,7 @@ from shapely.geometry import (
 
 from geopandas import GeoDataFrame, GeoSeries, read_file
 from geopandas.datasets import get_path
+import geopandas._compat as compat
 
 import pytest
 
@@ -277,6 +279,11 @@ class TestPointPlotting:
         np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
 
     def test_empty_plot(self):
+
+        s = GeoSeries([Polygon()])
+        with pytest.warns(UserWarning):
+            ax = s.plot()
+        assert len(ax.collections) == 0
         s = GeoSeries([])
         with pytest.warns(UserWarning):
             ax = s.plot()
@@ -286,6 +293,20 @@ class TestPointPlotting:
             ax = df.plot()
         assert len(ax.collections) == 0
 
+    def test_empty_geometry(self):
+
+        if compat.USE_PYGEOS:
+            s = GeoSeries([wkt.loads("POLYGON EMPTY")])
+            s = GeoSeries(
+                [Polygon([(0, 0), (1, 0), (1, 1)]), wkt.loads("POLYGON EMPTY")]
+            )
+            ax = s.plot()
+            assert len(ax.collections) == 1
+        if not compat.USE_PYGEOS:
+            s = GeoSeries([Polygon([(0, 0), (1, 0), (1, 1)]), Polygon()])
+            ax = s.plot()
+            assert len(ax.collections) == 1
+
     def test_multipoints(self):
 
         # MultiPoints