Repository: geopandas/geopandas

Issue:
Colors provided to plot color argument do not match patches
A geopandas dataframe has the option to plot the frame. This also has a color argument which can be provided a list of colors.

The colors do however not match the patches in each row, even though the list of colors provided is the same length as the number of rows in the geopandas data frame. 

Running the following code using the attached geojson file in [bug_df.zip](https://github.com/geopandas/geopandas/files/3451023/bug_df.zip) shows this :
```
import geopandas as gpd
import matplotlib.pyplot as plt
plot_df = gpd.read_file('bug_df.geojson')
plot_df['coords'] = plot_df['geometry'].apply(lambda x: \
                                    x.representative_point().coords[:][0])

fig, ax = plt.subplots(figsize=[15,15])
plot_df.plot(ax=ax, color=plot_df['color'])

for _, row in plot_df.iterrows():
    ax.annotate(s=row['color'], xy=row['coords'],
                horizontalalignment='center',
                color=row['color'])
plt.title('Wrong coloring')

fig, ax = plt.subplots(figsize=[15,15])

for col in plot_df['color'].unique():
    plot_df[plot_df['color']==col].plot(ax=ax, color=col)
plt.title('Correct coloring');
```

The output of this code shows that the colors for the patches do not match the expected color:
![image](https://user-images.githubusercontent.com/7702207/62202810-49693080-b38a-11e9-941f-c54472218887.png)

As a workaround (also in the code above) one can plot each of the patches separately with the right color. This gives the correct plot:
![image](https://user-images.githubusercontent.com/7702207/62202884-6c93e000-b38a-11e9-949b-42f6893078cc.png)



Passing in list of facecolors to gdf.plot() seems to have a bug
Passing in a list of facecolors to the gdf.plot() function appears to be broken - all features (some are polygons and some are multi-polygons) should be colored red except for one which should be orange.  However, the figure below generated using the code below shows two distinct polygons (not a multi-polygon) colored orange.

    import geopandas as gpd
    import matplotlib.pyplot as plt

    zctas_gdf = gpd.read_file('zcta/zcta.shp')

    fig, ax = plt.subplots(figsize=(12, 7))
    ax.set_aspect('equal')
    ax.axis('off')

    facecolor = ['red'] * len(zctas_gdf)
    facecolor[2] = 'orange'
    zctas_gdf.plot(ax=ax, alpha=0.85, facecolor=facecolor, edgecolor='black')

    plt.grid(False)
    plt.tight_layout()
    plt.show()
    plt.close()

[zcta.zip](https://github.com/geopandas/geopandas/files/2028062/zcta.zip)
![figure_1](https://user-images.githubusercontent.com/4724032/40382034-57141e2c-5dcb-11e8-9fea-b8b26256df2e.png)



Discussion:
The list of colors is matched with the list of patches, not rows. If your row contains MultiPolygons, which they do, it will not match. A workaround is to explode your gdf and use `color` column of exploded gdf (or plot it directly). But we should probably look at it and copy the same color for all parts of multipolygon. I've seen related issue, but can't find it now.

```
plot_dfex = plot_df.explode()
fig, ax = plt.subplots(figsize=[15,15])
plot_df.plot(ax=ax, color=plot_dfex['color'])
```

![plot](https://user-images.githubusercontent.com/36797143/62900220-87cefa00-bd51-11e9-9250-8f3463cf957a.png)
@martinfleis, thanks for the suggestion. I use the Bokeh package to visualize the plots and when I use explode disconnected land masses of the same region no longer seem to be connected. Not sure if that is another problem in itself though.

The list of colors matched to the patches itself would be a bit hard to manage for me. I have no idea how the externally downloaded shape file multipolygons are split into polygons. From my use case I just 'care' about the regions themselves, which is by gpd dataframe row which happens to contain a multipolygon in the geometry field.

I would be interested to hear the arguments for assigning the colors to the polygons. Perhaps there are use cases that I have not considered.
@bjlobo Thanks for the report.

This is actually not really supported, and working in certain cases 'by accident'. Currently the code simply passes the `facecolor` argument to the matplotlib collection constructor. That seems to work as long you have single geometries. But for MultiPolygons, we "flatten" the array of (Multi)Geometries to only single geometries. In that case, we should also insert duplicate values in the `facecolor` list of colors, as is done for values here:

https://github.com/geopandas/geopandas/blob/6e0fd9b18e5e992434143e79db72c20104e9f3ef/geopandas/plotting.py#L83

And it seems that matplotlib, if the `facecolors` list is too short, it just repeats the list, which is why a random other one gets colored orange.
BTW, I think a PR would certainly be welcome to try to fix this. We just need to think a bit about how to best handle the different cases (single string, array version of a color, 1D array of strings, 2D array of numeric rgb values, ..)
Sounds relatively easy.. will work on this, if no objection!
No objection from me if that is what you are asking, since I am not sure
what the solution is.

On Sat, Sep 8, 2018, 8:00 PM Philipp Kats <notifications@github.com> wrote:

> Sounds relatively easy.. will work on this, if no objection!
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/geopandas/geopandas/issues/730#issuecomment-419680891>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AEgVQM_yLHOUwGnC4qqdGxzFfW5HxbX-ks5uZFoWgaJpZM4UJKrB>
> .
>


Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -91,17 +91,24 @@ def plot_polygon_collection(
         )
     from matplotlib.collections import PatchCollection
 
-    geoms, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
 
     # PatchCollection does not accept some kwargs.
     if "markersize" in kwargs:
         del kwargs["markersize"]
-
-    # color=None overwrites specified facecolor/edgecolor with default color
     if color is not None:
         kwargs["color"] = color
+        if pd.api.types.is_list_like(color):
+            kwargs["color"] = np.take(color, multiindex)
+        else:
+            kwargs["color"] = color
+    else:
+        for att in ["facecolor", "edgecolor"]:
+            if att in kwargs:
+                if pd.api.types.is_list_like(kwargs[att]):
+                    kwargs[att] = np.take(kwargs[att], multiindex)
 
     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
 
@@ -145,9 +152,9 @@ def plot_linestring_collection(
     """
     from matplotlib.collections import LineCollection
 
-    geoms, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
 
     # LineCollection does not accept some kwargs.
     if "markersize" in kwargs:
@@ -155,7 +162,10 @@ def plot_linestring_collection(
 
     # color=None gives black instead of default color cycle
     if color is not None:
-        kwargs["color"] = color
+        if pd.api.types.is_list_like(color):
+            kwargs["color"] = np.take(color, multiindex)
+        else:
+            kwargs["color"] = color
 
     segments = [np.array(linestring)[:, :2] for linestring in geoms]
     collection = LineCollection(segments, **kwargs)
@@ -206,9 +216,10 @@ 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, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
+
     x = [p.x for p in geoms]
     y = [p.y for p in geoms]
 
@@ -218,9 +229,14 @@ def plot_point_collection(
     if markersize is not None:
         kwargs["s"] = markersize
 
+    if color is not None:
+        if pd.api.types.is_list_like(color):
+            color = np.take(color, multiindex)
+
     collection = ax.scatter(
         x, y, color=color, vmin=vmin, vmax=vmax, cmap=cmap, marker=marker, **kwargs
     )
+
     return collection
 
 
@@ -314,6 +330,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         facecolor = style_kwds.pop("facecolor", None)
         if color is not None:
             facecolor = color
+
         values_ = values[poly_idx] if cmap else None
         plot_polygon_collection(
             ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds


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
@@ -6,7 +6,15 @@
 import numpy as np
 
 from shapely.affinity import rotate
-from shapely.geometry import LineString, MultiPoint, MultiPolygon, Point, Polygon
+from shapely.geometry import (
+    MultiPolygon,
+    Polygon,
+    LineString,
+    Point,
+    MultiPoint,
+    MultiLineString,
+)
+
 
 from geopandas import GeoDataFrame, GeoSeries, read_file
 from geopandas.datasets import get_path
@@ -189,6 +197,10 @@ def test_multipoints(self):
         expected_colors = [cmap(0)] * self.N + [cmap(1)] * self.N
         _check_colors(2, ax.collections[0].get_facecolors(), expected_colors)
 
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiPolygon
+        _check_colors(2, ax.collections[0].get_facecolors(), ["r"] * 10 + ["b"] * 10)
+
 
 class TestPointZPlotting:
     def setup_method(self):
@@ -212,6 +224,12 @@ def setup_method(self):
         )
         self.df = GeoDataFrame({"geometry": self.lines, "values": values})
 
+        multiline1 = MultiLineString(self.lines.loc["A":"B"].values)
+        multiline2 = MultiLineString(self.lines.loc["C":"D"].values)
+        self.df2 = GeoDataFrame(
+            {"geometry": [multiline1, multiline2], "values": [0, 1]}
+        )
+
     def test_single_color(self):
 
         ax = self.lines.plot(color="green")
@@ -246,6 +264,23 @@ def test_style_kwargs(self):
             assert ls[0] == exp_ls[0]
             assert ls[1] == exp_ls[1]
 
+    def test_multilinestrings(self):
+
+        # MultiLineStrings
+        ax = self.df2.plot()
+        assert len(ax.collections[0].get_paths()) == 4
+        _check_colors(4, ax.collections[0].get_facecolors(), [MPL_DFT_COLOR] * 4)
+
+        ax = self.df2.plot("values")
+        cmap = plt.get_cmap(lut=2)
+        # colors are repeated for all components within a MultiLineString
+        expected_colors = [cmap(0), cmap(0), cmap(1), cmap(1)]
+        _check_colors(4, ax.collections[0].get_facecolors(), expected_colors)
+
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiLineString
+        _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
+
 
 class TestPolygonPlotting:
     def setup_method(self):
@@ -372,6 +407,10 @@ def test_multipolygons(self):
         expected_colors = [cmap(0), cmap(0), cmap(1), cmap(1)]
         _check_colors(4, ax.collections[0].get_facecolors(), expected_colors)
 
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiPolygon
+        _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
+
 
 class TestPolygonZPlotting:
     def setup_method(self):
@@ -673,6 +712,12 @@ def test_polygons(self):
         _check_colors(self.N, coll.get_edgecolor(), ["g"] * self.N)
         ax.cla()
 
+        # default: color can be passed as a list
+        coll = plot_polygon_collection(ax, self.polygons, color=["g", "b", "r"])
+        _check_colors(self.N, coll.get_facecolor(), ["g", "b", "r"])
+        _check_colors(self.N, coll.get_edgecolor(), ["g", "b", "r"])
+        ax.cla()
+
         # only setting facecolor keeps default for edgecolor
         coll = plot_polygon_collection(ax, self.polygons, facecolor="g")
         _check_colors(self.N, coll.get_facecolor(), ["g"] * self.N)