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
```

Discussion:
martinfleis: Thank you! I was thinking that it would be better to filter empty geoms out before `_flatten_multi_geoms`, but it probably does not matter much and this is quite readable. An array filter would probably require more changes, so I'd say that this is fine as it is now.

> What's the procedure in cases like that ? Should I submit a single pull request that closes the two issues ?

They are touching different parts of code, no need to worry about it.

> Also, should a test about plotting empty geometries be added ?

Yes please :).


gcaria: Thanks for your reply @martinfleis  !

While adding the test I've noticed that a warning is issued when an empty GeoSeries or an empty GeoDataFrame is plotted, which makes me wonder whether plotting a GeoSeries made only of empty geometries deserves a warning too.

Patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 25a62e9a..0257919f 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 e84dcfb6..b4df187e 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 d2485ed3..cc5c6eef 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 60c307ed..6653af38 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