Repository: geopandas/geopandas

Issue:
Controlling the colors
I'm trying to match the colors of two plots I create with gpd.plot() and plot() respectively.
The problem is that geopandas change the colors according to the length of the series.
I solved the problem by setting `cmap=plt.get_cmap('Set1')` but I think we better change the default behavior.

1. It appears to me that this function causes the issue:
```
def gencolor(N, colormap='Set1'):
    from matplotlib import cm
    # don't use more than 9 discrete colors
    n_colors = min(N, 9)
    cmap = cm.get_cmap(colormap, n_colors)
    colors = cmap(range(n_colors))
    for i in xrange(N):
        yield colors[i % n_colors]
```
Instead of `min(N,9)`, I think we should simply have `n_colors = 9`.
Pandas does not change the color scheme:
```
#in Pandas second color is blue
pd.DataFrame(pd.np.random.randn(2, 2)).plot()

#in GeoPandas second color is grey (I'd expect blue)
gpd.GeoSeries([Point(0,0),Point(0,1)]).plot()
```

2. It appears that Geopandas does not care about the plot style set in matplotlib. When I `plt.style.use('ggplot')` and then plot in geopandas, colors do not change (that is not the case for pandas). 

3. I tried setting colors as an iterable of hex (or rgba) while plotting, but neither worked for geopandas (though both work for pandas):
```
df = pd.DataFrame(np.random.randn(5, 5))
custom_colors = ['#FF0000', '#0000FF', '#FFFF00', '#000000', '#FFFFFF']
df.plot(color=custom_colors)
```
Setting a single color like `gpd.GeoSeries([...]).plot(color='#FF0000')` works, but passing multiple colors as a list gives `could not convert string to float: '#FF0000'` value error.



Discussion:

Patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index d705b911..b3b6460c 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -726,26 +726,6 @@ GON (((-122.84000 49.00000, -120.0000...
 
     nan_idx = np.asarray(pd.isna(values), dtype="bool")
 
-    # Define `values` as a Series
-    if categorical:
-        if cmap is None:
-            cmap = "tab10"
-
-        cat = pd.Categorical(values, categories=categories)
-        categories = list(cat.categories)
-
-        # values missing in the Categorical but not in original values
-        missing = list(np.unique(values[~nan_idx & cat.isna()]))
-        if missing:
-            raise ValueError(
-                "Column contains values not listed in categories. "
-                "Missing categories: {}.".format(missing)
-            )
-
-        values = cat.codes[~nan_idx]
-        vmin = 0 if vmin is None else vmin
-        vmax = len(categories) - 1 if vmax is None else vmax
-
     if scheme is not None:
         mc_err = (
             "The 'mapclassify' package (>= 2.4.0) is "
@@ -779,20 +759,46 @@ GON (((-122.84000 49.00000, -120.0000...
                     )
                 )
             else:
-                categories = list(legend_kwds.pop("labels"))
+                labels = list(legend_kwds.pop("labels"))
         else:
             fmt = "{:.2f}"
             if legend_kwds is not None and "fmt" in legend_kwds:
                 fmt = legend_kwds.pop("fmt")
 
-            categories = binning.get_legend_classes(fmt)
+            labels = binning.get_legend_classes(fmt)
             if legend_kwds is not None:
                 show_interval = legend_kwds.pop("interval", False)
             else:
                 show_interval = False
             if not show_interval:
-                categories = [c[1:-1] for c in categories]
-        values = np.array(binning.yb)
+                labels = [c[1:-1] for c in labels]
+
+        values = pd.Categorical([np.nan] * len(values), categories=labels, ordered=True)
+        values[~nan_idx] = pd.Categorical.from_codes(
+            binning.yb, categories=labels, ordered=True
+        )
+        if cmap is None:
+            cmap = "viridis"
+
+    # Define `values` as a Series
+    if categorical:
+        if cmap is None:
+            cmap = "tab10"
+
+        cat = pd.Categorical(values, categories=categories)
+        categories = list(cat.categories)
+
+        # values missing in the Categorical but not in original values
+        missing = list(np.unique(values[~nan_idx & cat.isna()]))
+        if missing:
+            raise ValueError(
+                "Column contains values not listed in categories. "
+                "Missing categories: {}.".format(missing)
+            )
+
+        values = cat.codes[~nan_idx]
+        vmin = 0 if vmin is None else vmin
+        vmax = len(categories) - 1 if vmax is None else vmax
 
     # fill values with placeholder where were NaNs originally to map them properly
     # (after removing them in categorical or scheme)


Test patch:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 712300e4..8e91ba1c 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -1069,6 +1069,10 @@ class TestMapclassifyPlotting:
         pth = get_path("naturalearth_lowres")
         cls.df = read_file(pth)
         cls.df["NEGATIVES"] = np.linspace(-10, 10, len(cls.df.index))
+        cls.df["low_vals"] = np.linspace(0, 0.3, cls.df.shape[0])
+        cls.df["mid_vals"] = np.linspace(0.3, 0.7, cls.df.shape[0])
+        cls.df["high_vals"] = np.linspace(0.7, 1.0, cls.df.shape[0])
+        cls.df.loc[cls.df.index[:20:2], "high_vals"] = np.nan
 
     def test_legend(self):
         with warnings.catch_warnings(record=True) as _:  # don't print warning
@@ -1209,6 +1213,126 @@ class TestMapclassifyPlotting:
         legend_height = _get_ax(fig, "fixed_colorbar").get_position().height
         assert abs(plot_height - legend_height) < 1e-6
 
+    def test_empty_bins(self):
+        bins = np.arange(1, 11) / 10
+        ax = self.df.plot(
+            "low_vals",
+            scheme="UserDefined",
+            classification_kwds={"bins": bins},
+            legend=True,
+        )
+        expected = np.array(
+            [
+                [0.281412, 0.155834, 0.469201, 1.0],
+                [0.267004, 0.004874, 0.329415, 1.0],
+                [0.244972, 0.287675, 0.53726, 1.0],
+            ]
+        )
+        assert all(
+            [
+                (z == expected).all(axis=1).any()
+                for z in ax.collections[0].get_facecolors()
+            ]
+        )
+        labels = [
+            "0.00, 0.10",
+            "0.10, 0.20",
+            "0.20, 0.30",
+            "0.30, 0.40",
+            "0.40, 0.50",
+            "0.50, 0.60",
+            "0.60, 0.70",
+            "0.70, 0.80",
+            "0.80, 0.90",
+            "0.90, 1.00",
+        ]
+        legend = [t.get_text() for t in ax.get_legend().get_texts()]
+        assert labels == legend
+
+        legend_colors_exp = [
+            (0.267004, 0.004874, 0.329415, 1.0),
+            (0.281412, 0.155834, 0.469201, 1.0),
+            (0.244972, 0.287675, 0.53726, 1.0),
+            (0.190631, 0.407061, 0.556089, 1.0),
+            (0.147607, 0.511733, 0.557049, 1.0),
+            (0.119699, 0.61849, 0.536347, 1.0),
+            (0.20803, 0.718701, 0.472873, 1.0),
+            (0.430983, 0.808473, 0.346476, 1.0),
+            (0.709898, 0.868751, 0.169257, 1.0),
+            (0.993248, 0.906157, 0.143936, 1.0),
+        ]
+
+        assert [
+            line.get_markerfacecolor() for line in ax.get_legend().get_lines()
+        ] == legend_colors_exp
+
+        ax2 = self.df.plot(
+            "mid_vals",
+            scheme="UserDefined",
+            classification_kwds={"bins": bins},
+            legend=True,
+        )
+        expected = np.array(
+            [
+                [0.244972, 0.287675, 0.53726, 1.0],
+                [0.190631, 0.407061, 0.556089, 1.0],
+                [0.147607, 0.511733, 0.557049, 1.0],
+                [0.119699, 0.61849, 0.536347, 1.0],
+                [0.20803, 0.718701, 0.472873, 1.0],
+            ]
+        )
+        assert all(
+            [
+                (z == expected).all(axis=1).any()
+                for z in ax2.collections[0].get_facecolors()
+            ]
+        )
+
+        labels = [
+            "-inf, 0.10",
+            "0.10, 0.20",
+            "0.20, 0.30",
+            "0.30, 0.40",
+            "0.40, 0.50",
+            "0.50, 0.60",
+            "0.60, 0.70",
+            "0.70, 0.80",
+            "0.80, 0.90",
+            "0.90, 1.00",
+        ]
+        legend = [t.get_text() for t in ax2.get_legend().get_texts()]
+        assert labels == legend
+        assert [
+            line.get_markerfacecolor() for line in ax2.get_legend().get_lines()
+        ] == legend_colors_exp
+
+        ax3 = self.df.plot(
+            "high_vals",
+            scheme="UserDefined",
+            classification_kwds={"bins": bins},
+            legend=True,
+        )
+        expected = np.array(
+            [
+                [0.709898, 0.868751, 0.169257, 1.0],
+                [0.993248, 0.906157, 0.143936, 1.0],
+                [0.430983, 0.808473, 0.346476, 1.0],
+            ]
+        )
+        assert all(
+            [
+                (z == expected).all(axis=1).any()
+                for z in ax3.collections[0].get_facecolors()
+            ]
+        )
+
+        legend = [t.get_text() for t in ax3.get_legend().get_texts()]
+        assert labels == legend
+
+        assert [
+            line.get_markerfacecolor() for line in ax3.get_legend().get_lines()
+        ] == legend_colors_exp
+
 
 class TestPlotCollections:
     def setup_method(self):