Repository: geopandas/geopandas

Issue:
BUG: Incorrect colours when plotting multiple geometry types with list of colours
**Expected result**

I have a `geopandas.GeoDataFrame` that contains multiple geometry types (e.g. `Polygon`, `LineString` and `Point`). I want to plot this data by supplying a list of colours (one per feature) to the `color=` parameter:

 ```
from shapely.geometry import LineString, Point, Polygon
import geopandas as gpd

gdf = gpd.GeoDataFrame(geometry=[Polygon([(1, 1), (1, 2), (2, 2), (2, 1)]),
                                 LineString([(2, 0), (4, 2)]),
                                 Point(3, 2),
                                 Point(1, 0)])

gdf.plot(color=['red', 'green', 'blue', 'blue'])
```

For this example, I would expect that:

1. My `Polygon` feature will be plotted in "red"
2. My `LineString` feature will be plotted in "green"
3. Both of my `Point` features will be plotted in "blue"

**Actual result**

However, I receive this result:

![image](https://user-images.githubusercontent.com/17680388/79415718-65346580-7ff1-11ea-88c6-0473ebaeaf1a.png)

It appears to me like the list of colours is being applied separately to each geometry type, with the first item of each type being plotted in "red", and the second item of each type being plotted in "green" (e.g. see the `Point` features).

This appears related to the PR and linked issues here (#1119), but those look like they refer specifically to multi-geometries, and **not multiple geometry types.**

**Environment**
`geopandas '0.7.0'`
`Python 3.6.9 (default, Nov  7 2019, 10:44:02) `
`IPython 7.12.0 `



Discussion:
Thank you for the report!

Your assumption is correct. Each geometry type is now plotted as an independent Collection, to which we pass `color` as it is (assuming it is a single value). For list-like `color` values of the correct length, we should do the same we do with `value`, i.e. decomposing the list based on the geom type.
ref #1385
Another example that I just saw on SO (https://stackoverflow.com/questions/67552817/geopandas-not-plotting-correct-colors):

```python
import geopandas
from shapely.geometry import Point, box

gdf = geopandas.GeoDataFrame({
    "geometry": [box(0,0,1,1), box(1,1,2,2), box(2,2,3,3), Point(0, 0), Point(1,1), Point(2,2), Point(3,3)],
    "color_rgba": [(0.0, 0.0, 0.0, 0.5), (0.0, 0.0, 0.0, 0.2), (0.0, 0.0, 0.0, 0.1), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0)],
})

gdf.plot(figsize=(20, 20), markersize=1000, edgecolor='k', color=gdf['color_rgba'])
```


![Figure_ge](https://user-images.githubusercontent.com/1020496/118391890-e0be7700-b636-11eb-95ef-7e1db891aef0.png)

(all points should be white)
I just got bitten by this issue; how do you recommend specifying color in a GeoDataFrame with mixed geometry types?
I mentioned it on the StackOverflow question as well: I think the easiest workaround for now is to plot the different geometry types separately (you can subset the single dataframe, once for the polygons, once for the linestrings, once for the points, and plot each of them separately) 
Using both examples in this issue:

```
import geopandas as gpd
import pandas as pd
from shapely.geometry import Point, box
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

# fmt: off
gdf = gpd.GeoDataFrame({
    "geometry": [box(0,0,1,1), box(1,1,2,2), box(2,2,3,3), Point(0, 0), Point(1,1), Point(2,2), Point(3,3)],
    "color_rgba": [(0.0, 0.0, 0.0, 0.5), (0.0, 0.0, 0.0, 0.2), (0.0, 0.0, 0.0, 0.1), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0)],
})
# fmt: on

fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(10, 6.4))
cmap = LinearSegmentedColormap.from_list(
    "seg", gdf["color_rgba"].unique(), N=len(gdf["color_rgba"].unique())

)

gdf.plot(ax=axs[0], markersize=1000, edgecolor="k", color=gdf["color_rgba"])
gdf.plot(ax=axs[1], markersize=1000, edgecolor="k", column="color_rgba", cmap="Blues")
gdf.plot(ax=axs[2], markersize=1000, edgecolor="k", column="color_rgba", cmap=cmap)
```
<img width="610" alt="image" src="https://user-images.githubusercontent.com/42769112/164683971-4b2dc052-d4d5-4fd6-901b-8031d3015007.png">

```
rom shapely.geometry import LineString, Point, Polygon
import geopandas as gpd
import numpy as np

gdf = gpd.GeoDataFrame(geometry=[Polygon([(1, 1), (1, 2), (2, 2), (2, 1)]),
                                LineString([(2, 0), (4, 2)]),
                                Point(3, 2),
                                Point(1, 0)])

fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 6.4))
colors = ['red', 'green', 'blue', 'blue']

cmap = LinearSegmentedColormap.from_list(
    "seg", np.unique(colors), N=len(np.unique(colors))

)
gdf.plot(ax=axs[0], color=colors)
gdf.assign(_color=colors).plot(ax=axs[1], column="_color", cmap=cmap)
```

<img width="609" alt="image" src="https://user-images.githubusercontent.com/42769112/164684152-92cccce6-1b7d-4c51-94c2-b2ba6f5b6949.png">

In both cases the situation is the **color** series / list is same length as dataframe.   
- could be considered an misunderstanding of **color** parameter. It's the sequence in which colors are used based on **column** parameter.  This is somewhat subtle as the way **geometry** is processed internally drives this
- could be considered an enhancement...  if length of **color** parameter is same as dataframe, create an internal column and a `LinearSegmentedColormap()` as per examples above

Happy to create a PR for second bullet if that's considered right approach
Great idea.. i can't speak for everyone but the GDF to me is the collection abstraction i am plotting, and i expected the color to distribute across all items irrespective of geometry type. I wonder what other side effects would occur with this patch?
> could be considered an misunderstanding of color parameter. It's the sequence in which colors are used based on column parameter.

Not necessarily. You can plot geometries without using a `column` and then the idea is that you are able to pass a list of colours that is mapped to individual geometries. It is just broken when you have mixed geom types.

> if length of color parameter is same as dataframe, create an internal column and a LinearSegmentedColormap() as per examples above

That may work, yes. You don't even need a column as `column` can take an array.

But the solution I outlined above would also work:

> For list-like color values of the correct length, we should do the same we do with value, i.e. decomposing the list based on the geom type.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -325,7 +325,7 @@ def plot_series(
     ----------
     s : Series
         The GeoSeries to be plotted. Currently Polygon,
-        MultiPolygon, LineString, MultiLineString and Point
+        MultiPolygon, LineString, MultiLineString, Point and MultiPoint
         geometries can be plotted.
     cmap : str (default None)
         The name of a colormap recognized by matplotlib. Any
@@ -335,7 +335,7 @@ def plot_series(
 
             tab10, tab20, Accent, Dark2, Paired, Pastel1, Set1, Set2
 
-    color : str (default None)
+    color : str, np.array, pd.Series, List (default None)
         If specified, all objects will be colored uniformly.
     ax : matplotlib.pyplot.Artist (default None)
         axes on which to draw the plot
@@ -414,6 +414,9 @@ def plot_series(
         )
         return ax
 
+    # have colors been given for all geometries?
+    color_given = pd.api.types.is_list_like(color) and len(color) == len(s)
+
     # if cmap is specified, create range of colors based on cmap
     values = None
     if cmap is not None:
@@ -426,6 +429,10 @@ def plot_series(
     # decompose GeometryCollections
     geoms, multiindex = _flatten_multi_geoms(s.geometry, prefix="Geom")
     values = np.take(values, multiindex, axis=0) if cmap else None
+    # ensure indexes are consistent
+    if color_given and isinstance(color, pd.Series):
+        color = color.reindex(s.index)
+    expl_color = np.take(color, multiindex, axis=0) if color_given else color
     expl_series = geopandas.GeoSeries(geoms)
 
     geom_types = expl_series.type
@@ -443,8 +450,9 @@ def plot_series(
         # color overrides both face and edgecolor. As we want people to be
         # able to use edgecolor as well, pass color to facecolor
         facecolor = style_kwds.pop("facecolor", None)
+        color_ = expl_color[poly_idx] if color_given else color
         if color is not None:
-            facecolor = color
+            facecolor = color_
 
         values_ = values[poly_idx] if cmap else None
         _plot_polygon_collection(
@@ -455,16 +463,20 @@ def plot_series(
     lines = expl_series[line_idx]
     if not lines.empty:
         values_ = values[line_idx] if cmap else None
+        color_ = expl_color[line_idx] if color_given else color
+
         _plot_linestring_collection(
-            ax, lines, values_, color=color, cmap=cmap, **style_kwds
+            ax, lines, values_, color=color_, cmap=cmap, **style_kwds
         )
 
     # plot all Points in the same collection
     points = expl_series[point_idx]
     if not points.empty:
         values_ = values[point_idx] if cmap else None
+        color_ = expl_color[point_idx] if color_given else color
+
         _plot_point_collection(
-            ax, points, values_, color=color, cmap=cmap, **style_kwds
+            ax, points, values_, color=color_, cmap=cmap, **style_kwds
         )
 
     plt.draw()
@@ -524,7 +536,7 @@ def plot_dataframe(
          - 'hexbin' : hexbin plot.
     cmap : str (default None)
         The name of a colormap recognized by matplotlib.
-    color : str (default None)
+    color : str, np.array, pd.Series (default None)
         If specified, all objects will be colored uniformly.
     ax : matplotlib.pyplot.Artist (default None)
         axes on which to draw the plot


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
@@ -16,6 +16,7 @@
     MultiPoint,
     MultiLineString,
     GeometryCollection,
+    box,
 )
 
 
@@ -871,6 +872,47 @@ def test_plot(self):
         self.df.plot()
 
 
+class TestColorParamArray:
+    def setup_method(self):
+        geom = []
+        color = []
+        for a, b in [(0, 2), (4, 6)]:
+            b = box(a, a, b, b)
+            geom += [b, b.buffer(0.8).exterior, b.centroid]
+            color += ["red", "green", "blue"]
+
+        self.gdf = GeoDataFrame({"geometry": geom, "color_rgba": color})
+        self.mgdf = self.gdf.dissolve(self.gdf.type)
+
+    def test_color_single(self):
+        ax = self.gdf.plot(color=self.gdf["color_rgba"])
+
+        _check_colors(
+            4,
+            np.concatenate([c.get_edgecolor() for c in ax.collections]),
+            ["green"] * 2 + ["blue"] * 2,
+        )
+        _check_colors(
+            4,
+            np.concatenate([c.get_facecolor() for c in ax.collections]),
+            ["red"] * 2 + ["blue"] * 2,
+        )
+
+    def test_color_multi(self):
+        ax = self.mgdf.plot(color=self.mgdf["color_rgba"])
+
+        _check_colors(
+            4,
+            np.concatenate([c.get_edgecolor() for c in ax.collections]),
+            ["green"] * 2 + ["blue"] * 2,
+        )
+        _check_colors(
+            4,
+            np.concatenate([c.get_facecolor() for c in ax.collections]),
+            ["red"] * 2 + ["blue"] * 2,
+        )
+
+
 class TestGeometryCollectionPlotting:
     def setup_method(self):
         coll1 = GeometryCollection(