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:
Thanks for the report! I don't think you are doing something wrong, this is a bug (or, a not yet supported case). It would be a nice enhancement to actually support this.
You're welcome, I'm noticing this may be a more fundamental problem, since trying to write the geodataframe to a file causes problems as reported here https://github.com/geopandas/geopandas/issues/867. Is there a way to prevent the intersection from generating GeometryCollections, MultiPolygons, or anything that would be problematic when plotting/writing to a file?
It might certainly be a more general problem that GeometryCollections are less good supported and tested, but the underlying code-related problem for the I/O or the plotting here are completely independent of each other. 

> Is there a way to prevent the intersection from generating GeometryCollections, MultiPolygons, or anything that would be problematic when plotting/writing to a file?

Not sure about that, sorry. I think the intersection does that here by definition, because of the layout of the two lines (you can't represent the result as a LineString). 

Something that geopandas can do is provide functionality to make sure you have a uniform geometry type, as some formats need that (eg if you have a mixture of Polygons and MultiPolygons, make sure that everything is converted to MultiPolygons)


The functionality to ensure uniform geometry type is implemented already? What specific methods are you referring to? That might solve my problem, if I can convert everything to linestring for example...


No, it is something that GeoPandas could provide, but currently doesn't.
To reiterate @jorisvandenbossche's point, there's not really a way to express `x` as a "uniform geometry type" :/

The main idea behind the `GeometryCollection` in OGC is to wrap a `MULTI-`type shape with heterogeneous types. In your example... `x` has both point and linestring components: 
```python
list(x.geoms)
[<shapely.geometry.point.Point at 0x7fba55a66828>,
 <shapely.geometry.linestring.LineString at 0x7fba55a66b00>]
```
What is the intended "uniform" type for those in your data?

You could "explode" `gdf` to get its constituent parts, if that helps:
```python
gdf.explode()
                  geometry
0 0            POINT (2 2)
  1  LINESTRING (0 0, 1 1)
```
This should let you plot all the elements fine using `gdf.explode().plot()`. 
*edit: should let you plot all the elements for one definition of "fine." if you had an attribute, using `gdf.explode().plot('attribute')` would treat each row as its own observation, so any map classification of `'attribute'` might get thrown off by the "extra" geometries.*

But, if you wanted to write that out to a file, you'd have the heterogeneous type problem discussed in #867 

Is anyone working on this enhancement? If not I could try to pick it up at some point...
Not that I am aware of, so feel free to pick it up. If you have any questions, don't hesitate to ask here.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -3,45 +3,41 @@
 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
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -8,9 +8,11 @@
     MultiPolygon,
     Polygon,
     LineString,
+    LinearRing,
     Point,
     MultiPoint,
     MultiLineString,
+    GeometryCollection,
 )
 
 
@@ -294,6 +296,12 @@ def setup_method(self):
             {"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 @@ def test_single_color(self):
         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 @@ def test_plot(self):
         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")