Repository: geopandas/geopandas

Issue:
geopandas fails to plot GeometryCollection
It looks like geometry collections are not supported by the geopandas `plot` feature. The following will generate an empty plot.
```
import shapely
import geopandas
a = shapely.geometry.LineString([(0, 0), (1, 1), (1,2), (2,2)])
b = shapely.geometry.LineString([(0, 0), (1, 1), (2,1), (2,2)])
x = a.intersection(b)
gdf = geopandas.GeoDataFrame(geometry=[x])
gdf.plot();
```

Am I doing something wrong or is this a bug ? If so how to fix or workaround? Thanks.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=h1) Report
> Merging [#1225](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/6ac19052c811b39fea88b40625c69246f733709d?src=pr&el=desc) will **decrease** coverage by `0.11%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/geopandas/geopandas/pull/1225/graphs/tree.svg?width=650&token=6TOPMcCBhB&height=150&src=pr)](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1225      +/-   ##
==========================================
- Coverage   90.05%   89.94%   -0.12%     
==========================================
  Files          21       21              
  Lines        2082     2088       +6     
==========================================
+ Hits         1875     1878       +3     
- Misses        207      210       +3
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/plotting.py](https://codecov.io/gh/geopandas/geopandas/pull/1225/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Bsb3R0aW5nLnB5) | `92.06% <100%> (-0.54%)` | :arrow_down: |
| [geopandas/array.py](https://codecov.io/gh/geopandas/geopandas/pull/1225/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2FycmF5LnB5) | `93.76% <0%> (-0.23%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=footer). Last update [6ac1905...97101d3](https://codecov.io/gh/geopandas/geopandas/pull/1225?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

martinfleis: @jorisvandenbossche I have made changes based on the review, apart from import/GeometryArray/GeoSeries part (see above).

Patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index c86509aa..f915a569 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -3,45 +3,41 @@ import warnings
 import numpy as np
 import pandas as pd
 
+import geopandas
 
-def _flatten_multi_geoms(geoms, colors=None):
+
+def _flatten_multi_geoms(geoms, prefix="Multi"):
     """
-    Returns Series like geoms and colors, except that any Multi geometries
-    are split into their components and colors are repeated for all component
-    in the same Multi geometry.  Maintains 1:1 matching of geometry to color.
-    Passing `color` is optional, and when no `color` is passed a list of None
-    values is returned as `component_colors`.
+    Returns Series like geoms and index, except that any Multi geometries
+    are split into their components and indices are repeated for all component
+    in the same Multi geometry.  Maintains 1:1 matching of geometry to value.
 
-    "Colors" are treated opaquely and so can actually contain any values.
+    Prefix specifies type of geometry to be flatten. 'Multi' for MultiPoint and similar,
+    "Geom" for GeometryCollection.
 
     Returns
     -------
 
     components : list of geometry
 
-    component_colors : list of whatever type `colors` contains
+    component_index : index array
+        indices are repeated for all components in the same Multi geometry
     """
-    if colors is None:
-        colors = [None] * len(geoms)
-
-    components, component_colors = [], []
+    components, component_index = [], []
 
-    if not geoms.geom_type.str.startswith("Multi").any():
-        return geoms, colors
+    if not geoms.geom_type.str.startswith(prefix).any():
+        return geoms, np.arange(len(geoms))
 
-    # precondition, so zip can't short-circuit
-    assert len(geoms) == len(colors)
-    for geom, color in zip(geoms, colors):
-        if geom.type.startswith("Multi"):
+    for ix, geom in enumerate(geoms):
+        if geom.type.startswith(prefix):
             for poly in geom:
                 components.append(poly)
-                # repeat same color for all components
-                component_colors.append(color)
+                component_index.append(ix)
         else:
             components.append(geom)
-            component_colors.append(color)
+            component_index.append(ix)
 
-    return components, component_colors
+    return components, np.array(component_index)
 
 
 def plot_polygon_collection(
@@ -92,7 +88,7 @@ def plot_polygon_collection(
     from matplotlib.collections import PatchCollection
     from matplotlib.colors import is_color_like
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -165,7 +161,7 @@ def plot_linestring_collection(
     from matplotlib.collections import LineCollection
     from matplotlib.colors import is_color_like
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -236,7 +232,7 @@ def plot_point_collection(
     if values is not None and color is not None:
         raise ValueError("Can only specify one of 'values' and 'color' kwargs")
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -349,16 +345,22 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         style_kwds["vmin"] = style_kwds.get("vmin", values.min())
         style_kwds["vmax"] = style_kwds.get("vmax", values.max())
 
-    geom_types = s.geometry.type
+    # decompose GeometryCollections
+    geoms, multiindex = _flatten_multi_geoms(s.geometry, prefix="Geom")
+    values = np.take(values, multiindex, axis=0) if cmap else None
+    expl_series = geopandas.GeoSeries(geoms)
+
+    geom_types = expl_series.type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
-        (geom_types == "LineString") | (geom_types == "MultiLineString")
+        (geom_types == "LineString")
+        | (geom_types == "MultiLineString")
+        | (geom_types == "LinearRing")
     )
     point_idx = np.asarray((geom_types == "Point") | (geom_types == "MultiPoint"))
 
     # plot all Polygons and all MultiPolygon components in the same collection
-    polys = s.geometry[poly_idx]
-
+    polys = expl_series[poly_idx]
     if not polys.empty:
         # color overrides both face and edgecolor. As we want people to be
         # able to use edgecolor as well, pass color to facecolor
@@ -372,7 +374,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         )
 
     # plot all LineStrings and MultiLineString components in same collection
-    lines = s.geometry[line_idx]
+    lines = expl_series[line_idx]
     if not lines.empty:
         values_ = values[line_idx] if cmap else None
         plot_linestring_collection(
@@ -380,7 +382,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         )
 
     # plot all Points in the same collection
-    points = s.geometry[point_idx]
+    points = expl_series[point_idx]
     if not points.empty:
         values_ = values[point_idx] if cmap else None
         plot_point_collection(ax, points, values_, color=color, cmap=cmap, **style_kwds)
@@ -595,15 +597,23 @@ def plot_dataframe(
     mn = values[~np.isnan(values)].min() if vmin is None else vmin
     mx = values[~np.isnan(values)].max() if vmax is None else vmax
 
-    geom_types = df.geometry.type
+    # decompose GeometryCollections
+    geoms, multiindex = _flatten_multi_geoms(df.geometry, prefix="Geom")
+    values = np.take(values, multiindex, axis=0)
+    nan_idx = np.take(nan_idx, multiindex, axis=0)
+    expl_series = geopandas.GeoSeries(geoms)
+
+    geom_types = expl_series.type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
-        (geom_types == "LineString") | (geom_types == "MultiLineString")
+        (geom_types == "LineString")
+        | (geom_types == "MultiLineString")
+        | (geom_types == "LinearRing")
     )
     point_idx = np.asarray((geom_types == "Point") | (geom_types == "MultiPoint"))
 
     # plot all Polygons and all MultiPolygon components in the same collection
-    polys = df.geometry[poly_idx & np.invert(nan_idx)]
+    polys = expl_series[poly_idx & np.invert(nan_idx)]
     subset = values[poly_idx & np.invert(nan_idx)]
     if not polys.empty:
         plot_polygon_collection(
@@ -611,7 +621,7 @@ def plot_dataframe(
         )
 
     # plot all LineStrings and MultiLineString components in same collection
-    lines = df.geometry[line_idx & np.invert(nan_idx)]
+    lines = expl_series[line_idx & np.invert(nan_idx)]
     subset = values[line_idx & np.invert(nan_idx)]
     if not lines.empty:
         plot_linestring_collection(
@@ -619,10 +629,11 @@ def plot_dataframe(
         )
 
     # plot all Points in the same collection
-    points = df.geometry[point_idx & np.invert(nan_idx)]
+    points = expl_series[point_idx & np.invert(nan_idx)]
     subset = values[point_idx & np.invert(nan_idx)]
     if not points.empty:
         if isinstance(markersize, np.ndarray):
+            markersize = np.take(markersize, multiindex, axis=0)
             markersize = markersize[point_idx & np.invert(nan_idx)]
         plot_point_collection(
             ax,
@@ -643,7 +654,7 @@ def plot_dataframe(
         merged_kwds = style_kwds.copy()
         merged_kwds.update(missing_kwds)
 
-        plot_series(df.geometry[nan_idx], ax=ax, **merged_kwds)
+        plot_series(expl_series[nan_idx], ax=ax, **merged_kwds)
 
     if legend and not color:
 


Test patch:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index a6b66201..306e3b6c 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -8,9 +8,11 @@ from shapely.geometry import (
     MultiPolygon,
     Polygon,
     LineString,
+    LinearRing,
     Point,
     MultiPoint,
     MultiLineString,
+    GeometryCollection,
 )
 
 
@@ -294,6 +296,12 @@ class TestLineStringPlotting:
             {"geometry": [multiline1, multiline2], "values": [0, 1]}
         )
 
+        self.linearrings = GeoSeries(
+            [LinearRing([(0, i), (4, i + 0.5), (9, i)]) for i in range(self.N)],
+            index=list("ABCDEFGHIJ"),
+        )
+        self.df3 = GeoDataFrame({"geometry": self.linearrings, "values": values})
+
     def test_single_color(self):
 
         ax = self.lines.plot(color="green")
@@ -302,6 +310,12 @@ class TestLineStringPlotting:
         ax = self.df.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
 
+        ax = self.linearrings.plot(color="green")
+        _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
+
+        ax = self.df3.plot(color="green")
+        _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
+
         # check rgba tuple GH1178
         ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
         _check_colors(
@@ -552,6 +566,37 @@ class TestPolygonZPlotting:
         self.df.plot()
 
 
+class TestGeometryCollectionPlotting:
+    def setup_method(self):
+        coll1 = GeometryCollection(
+            [
+                Polygon([(1, 0), (2, 0), (2, 1)]),
+                MultiLineString([((0.5, 0.5), (1, 1)), ((1, 0.5), (1.5, 1))]),
+            ]
+        )
+        coll2 = GeometryCollection(
+            [Point(0.75, 0.25), Polygon([(2, 2), (3, 2), (2, 3)])]
+        )
+
+        self.series = GeoSeries([coll1, coll2])
+        self.df = GeoDataFrame({"geometry": self.series, "values": [1, 2]})
+
+    def test_colors(self):
+        # default uniform color
+        ax = self.series.plot()
+        _check_colors(1, ax.collections[0].get_facecolors(), [MPL_DFT_COLOR])  # poly
+        _check_colors(2, ax.collections[1].get_edgecolors(), [MPL_DFT_COLOR])  # line
+        _check_colors(2, ax.collections[2].get_facecolors(), [MPL_DFT_COLOR])  # point
+
+    def test_values(self):
+        ax = self.df.plot("values")
+        cmap = plt.get_cmap()
+        exp_colors = cmap(np.arange(2) / 1)
+        _check_colors(1, ax.collections[0].get_facecolors(), exp_colors)  # poly
+        _check_colors(2, ax.collections[1].get_edgecolors(), [exp_colors[0]])  # line
+        _check_colors(2, ax.collections[2].get_facecolors(), [exp_colors[1]])  # point
+
+
 class TestNonuniformGeometryPlotting:
     def setup_method(self):
         pytest.importorskip("matplotlib", "1.5.0")