Repository: geopandas/geopandas

Issue:
BUG: Passing RGBA tuple to color parameter in .plot() throws error
*tl;dr:* `GeoDataFrame.plot()` throws an error when passing a RGB/RGBA tuple as the `color` parameter (e.g. `color=(0.5, 0.5, 0.5)`). These issues do not persist when passing string color names (e.g `color='gray'`) or hex codes (e.g. `color='#808080'`); it appears to have been introduced in #1119 when introducing support for list-like colors.

The issue also holds for `LineString`/`MultiLineString` collections and `Point`/`MultiPoint` collections; I'll explain the issue only for `Polygons` and `MultiPolygons`. **I've added instructions for reproducible examples at the bottom of this issue**.

## Description

When passing an RGB tuple as the `color` parameter, two types of errors can be thrown. I will refer to "total Polygons" as the number of flattened geometries that result from `_flatten_multi_geoms`:
1. When there are 4 or more total Polygons, an `IndexError` is thrown in line 111 because `multiindex = range(len(geoms))` (from [line 94](https://github.com/geopandas/geopandas/blob/2c5bfc5b98a9364a205354297d9e9c4786a4f6af/geopandas/plotting.py#L94)), and `kwargs['facecolor']` is only length 3 (the RGB tuple).
1. When there are 1 or 2 total Polygons, the `IndexError` is not an issue because `kwargs['facecolor']` is length 3 (the RGB tuple) and `multiindex = range(len(geoms))` is length 1 or 2, and thus `np.take` will never be out of bounds. After running line 111, `kwargs['facecolor']` is reassigned to an array of length 1 or 2; thus a `ValueError` is thrown in line 113, where `PatchCollection()` attempts to assign a float (e.g. `0.5`) to the geometry's color.

https://github.com/geopandas/geopandas/blob/2c5bfc5b98a9364a205354297d9e9c4786a4f6af/geopandas/plotting.py#L107-L113

When the number of total Polygons equals 3 (e.g. 3 Polygons, or 2 Polygons and 1 MultiPolygon consisting of 2 Polygons, etc), `kwargs['facecolor']` will be assigned to the entire RGB sequence in line 111, and thus no errors are thrown.

Furthermore, when passing in an RGBA, similar behavior is observed; the correct color appears when there are 4 total Polygons, whereas Error 1 is observed for >=5 total Polygons, and Error 2 is observed for <=3 total Polygons.

## Reproducible example

I've reproduced Error 1 and Error 2 in 0.5.1 ([requirements051.txt](https://github.com/geopandas/geopandas/files/3758125/requirements051.txt)) where functionality works as expected, as well as in 0.6.0 ([requirements060.txt](https://github.com/geopandas/geopandas/files/3758130/requirements060.txt)) which throws errors. Each of these environments can be reproduced with the following bash code (replacing 060 with 051 where appropriate):

```bash
python3 -m venv gpd060
source gpd060/bin/activate
pip install -r requirements060.txt
```

Below is my code for reproducible examples, as well as corresponding output and errors from 0.6.0:

### Error 1

```python
import geopandas as gpd
from shapely.geometry import Polygon

df_error_1 = gpd.GeoDataFrame(
    {'geometry':[Polygon([(1, 1), (4, 1), (4, 3)]), 
                 Polygon([(1, 2), (1, 4), (4, 4)]),
                 Polygon([(1, 5), (4, 5), (4, 7)]),
                 Polygon([(1, 6), (1, 8), (4, 8)])]}
)

df_error_1.plot() # runs fine
df_error_1.plot(color='gray') # runs fine
df_error_1.plot(color='#808080') # runs fine
df_error_1.plot(color=(0.5, 0.5, 0.5)) # throws error, see below
```

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/geodataframe.py", line 606, in plot
    return plot_dataframe(self, *args, **kwargs)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 502, in plot_dataframe
    **style_kwds
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 341, in plot_series
    ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 111, in plot_polygon_collection
    kwargs[att] = np.take(kwargs[att], multiindex)
  File "<__array_function__ internals>", line 6, in take
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/numpy/core/fromnumeric.py", line 194, in take
    return _wrapfunc(a, 'take', indices, axis=axis, out=out, mode=mode)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/numpy/core/fromnumeric.py", line 58, in _wrapfunc
    return _wrapit(obj, method, *args, **kwds)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/numpy/core/fromnumeric.py", line 47, in _wrapit
    result = getattr(asarray(obj), method)(*args, **kwds)
IndexError: index 3 is out of bounds for size 3
```

### Error 2

```python
df_error_2 = gpd.GeoDataFrame(
    {'geometry':[Polygon([(1, 1), (4, 1), (4, 3)]), 
                 Polygon([(1, 6), (1, 8), (4, 8)])]}
)

df_error_2.plot() # runs fine
df_error_2.plot(color='gray') # runs fine
df_error_2.plot(color='#808080') # runs fine
df_error_2.plot(color=(0.5, 0.5, 0.5)) # throws error, see below
```
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/geodataframe.py", line 606, in plot
    return plot_dataframe(self, *args, **kwargs)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 502, in plot_dataframe
    **style_kwds
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 341, in plot_series
    ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/geopandas/plotting.py", line 113, in plot_polygon_collection
    collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/collections.py", line 1805, in __init__
    Collection.__init__(self, **kwargs)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/collections.py", line 162, in __init__
    self.update(kwargs)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/artist.py", line 974, in update
    ret = [_update_property(self, k, v) for k, v in props.items()]
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/artist.py", line 974, in <listcomp>
    ret = [_update_property(self, k, v) for k, v in props.items()]
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/artist.py", line 971, in _update_property
    return func(v)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/collections.py", line 678, in set_facecolor
    self._set_facecolor(c)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/collections.py", line 661, in _set_facecolor
    self._facecolors = mcolors.to_rgba_array(c, self._alpha)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/colors.py", line 294, in to_rgba_array
    result[i] = to_rgba(cc, alpha)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/colors.py", line 177, in to_rgba
    rgba = _to_rgba_no_colorcycle(c, alpha)
  File "/Users/junseopark/Documents/gpd_test/gpd060/lib/python3.7/site-packages/matplotlib/colors.py", line 240, in _to_rgba_no_colorcycle
    raise ValueError("Invalid RGBA argument: {!r}".format(orig_c))
ValueError: Invalid RGBA argument: 0.5
```


Discussion:
Thank you for the thorough report!

Your observation and reasoning makes sense to me. As you already found where the issue is, would you be willing to do a bug fix PR?

We also should add this to tests as this situation is clearly uncovered right now.
Yes, I can work on a PR and add test cases in the coming days!
@junseo-park great! Thank you. If you'll need anything, ask here or open PR with proposed changes and we can discuss there.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -88,6 +88,7 @@ def plot_polygon_collection(
             "The descartes package is required for plotting polygons in geopandas."
         )
     from matplotlib.collections import PatchCollection
+    from matplotlib.colors import is_color_like
 
     geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
     if values is not None:
@@ -97,16 +98,26 @@ def plot_polygon_collection(
     if "markersize" in kwargs:
         del kwargs["markersize"]
     if color is not None:
-        kwargs["color"] = color
-        if pd.api.types.is_list_like(color):
+        if is_color_like(color):
+            kwargs["color"] = color
+        elif pd.api.types.is_list_like(color):
             kwargs["color"] = np.take(color, multiindex)
         else:
-            kwargs["color"] = color
+            raise TypeError(
+                "Color attribute has to be a single color or sequence of colors."
+            )
+
     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)
+                if not is_color_like(kwargs[att]):
+                    if pd.api.types.is_list_like(kwargs[att]):
+                        kwargs[att] = np.take(kwargs[att], multiindex)
+                    elif kwargs[att] is not None:
+                        raise TypeError(
+                            "Color attribute has to be a single color or sequence "
+                            "of colors."
+                        )
 
     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
 
@@ -150,6 +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)))
     if values is not None:
@@ -161,10 +173,14 @@ def plot_linestring_collection(
 
     # color=None gives black instead of default color cycle
     if color is not None:
-        if pd.api.types.is_list_like(color):
+        if is_color_like(color):
+            kwargs["color"] = color
+        elif pd.api.types.is_list_like(color):
             kwargs["color"] = np.take(color, multiindex)
         else:
-            kwargs["color"] = color
+            raise TypeError(
+                "Color attribute has to be a single color or sequence of colors."
+            )
 
     segments = [np.array(linestring)[:, :2] for linestring in geoms]
     collection = LineCollection(segments, **kwargs)
@@ -213,6 +229,8 @@ def plot_point_collection(
     -------
     collection : matplotlib.collections.Collection that was plotted
     """
+    from matplotlib.colors import is_color_like
+
     if values is not None and color is not None:
         raise ValueError("Can only specify one of 'values' and 'color' kwargs")
 
@@ -230,8 +248,13 @@ def plot_point_collection(
         kwargs["s"] = markersize
 
     if color is not None:
-        if pd.api.types.is_list_like(color):
-            color = np.take(color, multiindex)
+        if not is_color_like(color):
+            if pd.api.types.is_list_like(color):
+                color = np.take(color, multiindex)
+            else:
+                raise TypeError(
+                    "Color attribute has to be a single color or sequence of colors."
+                )
 
     if "norm" not in kwargs:
         collection = ax.scatter(


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
@@ -117,6 +117,18 @@ def test_single_color(self):
         ax = self.df.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_facecolors(), ["green"] * self.N)
 
+        # check rgba tuple GH1178
+        ax = self.df.plot(color=(0.5, 0.5, 0.5))
+        _check_colors(
+            self.N, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5)] * self.N
+        )
+        ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
+        _check_colors(
+            self.N, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * self.N
+        )
+        with pytest.raises(TypeError):
+            self.df.plot(color="not color")
+
         with warnings.catch_warnings(record=True) as _:  # don't print warning
             # 'color' overrides 'column'
             ax = self.df.plot(column="values", color="green")
@@ -266,6 +278,18 @@ def test_single_color(self):
         ax = self.df.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(
+            self.N, ax.collections[0].get_colors(), [(0.5, 0.5, 0.5, 0.5)] * self.N
+        )
+        ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
+        _check_colors(
+            self.N, ax.collections[0].get_colors(), [(0.5, 0.5, 0.5, 0.5)] * self.N
+        )
+        with pytest.raises(TypeError):
+            self.df.plot(color="not color")
+
         with warnings.catch_warnings(record=True) as _:  # don't print warning
             # 'color' overrides 'column'
             ax = self.df.plot(column="values", color="green")
@@ -352,6 +376,14 @@ def test_single_color(self):
         _check_colors(2, ax.collections[0].get_facecolors(), ["green"] * 2)
         _check_colors(2, ax.collections[0].get_edgecolors(), ["k"] * 2)
 
+        # check rgba tuple GH1178
+        ax = self.df.plot(color=(0.5, 0.5, 0.5))
+        _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5)] * 2)
+        ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
+        _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * 2)
+        with pytest.raises(TypeError):
+            self.df.plot(color="not color")
+
         with warnings.catch_warnings(record=True) as _:  # don't print warning
             # 'color' overrides 'values'
             ax = self.df.plot(column="values", color="green")
@@ -402,6 +434,17 @@ def test_style_kwargs(self):
         _check_colors(2, ax.collections[0].get_facecolors(), ["g"] * 2, alpha=0.4)
         _check_colors(2, ax.collections[0].get_edgecolors(), ["r"] * 2, alpha=0.4)
 
+        # check rgba tuple GH1178 for face and edge
+        ax = self.df.plot(facecolor=(0.5, 0.5, 0.5), edgecolor=(0.4, 0.5, 0.6))
+        _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5)] * 2)
+        _check_colors(2, ax.collections[0].get_edgecolors(), [(0.4, 0.5, 0.6)] * 2)
+
+        ax = self.df.plot(
+            facecolor=(0.5, 0.5, 0.5, 0.5), edgecolor=(0.4, 0.5, 0.6, 0.5)
+        )
+        _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * 2)
+        _check_colors(2, ax.collections[0].get_edgecolors(), [(0.4, 0.5, 0.6, 0.5)] * 2)
+
     def test_legend_kwargs(self):
 
         ax = self.df.plot(
@@ -665,6 +708,10 @@ def test_points(self):
         _check_colors(self.N, coll.get_edgecolors(), ["r", "g", "b"])
         ax.cla()
 
+        # not a color
+        with pytest.raises(TypeError):
+            plot_point_collection(ax, self.points, color="not color")
+
     def test_points_values(self):
         from geopandas.plotting import plot_point_collection
 
@@ -716,6 +763,10 @@ def test_linestrings(self):
         assert res_ls[1] == exp_ls[1]
         ax.cla()
 
+        # not a color
+        with pytest.raises(TypeError):
+            plot_linestring_collection(ax, self.lines, color="not color")
+
     def test_linestrings_values(self):
         from geopandas.plotting import plot_linestring_collection
 
@@ -784,6 +835,10 @@ def test_polygons(self):
         _check_colors(self.N, coll.get_edgecolor(), ["r"] * self.N)
         ax.cla()
 
+        # not a color
+        with pytest.raises(TypeError):
+            plot_polygon_collection(ax, self.polygons, color="not color")
+
     def test_polygons_values(self):
         from geopandas.plotting import plot_polygon_collection