Repository: geopandas/geopandas

Issue:
Consistent legend colors over subplots 
I am plotting two maps, each associated with a column (`c1`, `c2`), and with a legend. 
I want a consistent coloring for the two maps, however, as in right map one categorical feature is missing (`feature1`), the colors change too.

Basically I am looking for a way of mapping a color to a specific categorical feature over subplots, both shown on the map as well as in the legend, no matter if the actual feature value is in the selected column or not. 


```
from shapely.geometry import Polygon
from matplotlib import colors   
import matplotlib.pyplot as plt
import geopandas as gpd

# create sample data
p1 = Polygon([(0.5, 0.5), (1, 0.5), (1, 1), (0.5, 1)])
p2 = Polygon([(0, 0), (0.5, 0), (0.5, 0.5), (0, 0.5)])
p3 = Polygon([(0, 0.5), (0.5, 0.5), (0.5, 1), (0,1)])
gdf = gpd.GeoDataFrame({"c1" : ["feature1","feature2","feature3"],
                        "c2" : ["feature2","feature3","feature3"]},
                       geometry=gpd.GeoSeries((p1,p2,p3)))

# colormap
hmap = colors.ListedColormap(['lightgrey', 'red', 'lightblue'])

# ploting
fig, ax = plt.subplots(ncols=2)
gdf.plot(column="c1", legend=True, categorical=True, cmap=hmap, ax=ax[0])
gdf.plot(column="c2", legend=True, categorical=True, cmap=hmap, ax=ax[1])
```

![test](https://user-images.githubusercontent.com/11331549/50737326-13831580-11c8-11e9-9b9b-fd60210415b8.png)



Discussion:
Hi, I'm facing a similar issue plotting points with different ranges of values. I'd like to be able to compare the different plots with an unique legend.

I tried to understand if there were solutions in matplotlib that could be used for this particular case. I've found two cases:
- [Multiple Image](https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/multi_image.html#sphx-glr-gallery-images-contours-and-fields-multi-image-py)
- [Subplots Adjust](https://matplotlib.org/3.1.1/gallery/subplots_axes_and_figures/subplots_adjust.html#sphx-glr-gallery-subplots-axes-and-figures-subplots-adjust-py)

I'm quite new in coding and actually I'm not really sure if this could help, but is there any way to make something similar with geopandas? 

``` python
N1 = 10
points_1 = gpd.GeoSeries(Point(i, i) for i in range(N1))
values_1 = np.arange(N1)
N2 = 4
points_2 = gpd.GeoSeries(Point(i, i) for i in range(N2))
values_2 = np.arange(N2)

df1 = gpd.GeoDataFrame({"geometry": points_1, "values": values_1})
df2 = gpd.GeoDataFrame({"geometry": points_2, "values": values_2})

fig = plt.figure()
fig.set_size_inches(10, 10)

ax1=plt.subplot(221) 
df1.plot(ax=ax1, column='values', legend = True)

ax2=plt.subplot(222) 
df2.plot(ax=ax2, column='values', legend = True)

plt.show()
```
![legend](https://user-images.githubusercontent.com/17175539/67012329-7dcecc00-f0f1-11e9-82db-5b0cab311238.png)

@kerooksblog You have to use the same normalisation for both plots. Otherwise it normalise cmap separately for both plots.

```py
from matplotlib.colors import Normalize

N1 = 10
points_1 = gpd.GeoSeries(Point(i, i) for i in range(N1))
values_1 = np.arange(N1)
N2 = 4
points_2 = gpd.GeoSeries(Point(i, i) for i in range(N2))
values_2 = np.arange(N2)

df1 = gpd.GeoDataFrame({"geometry": points_1, "values": values_1})
df2 = gpd.GeoDataFrame({"geometry": points_2, "values": values_2})

fig = plt.figure()
fig.set_size_inches(10, 10)

ax1=plt.subplot(221)
norm = Normalize(vmin=df1['values'].min(), vmax=df1['values'].max())
df1.plot(ax=ax1, column='values', legend = True, norm=norm)

ax2=plt.subplot(222) 
df2.plot(ax=ax2, column='values', legend = True, norm=norm)

plt.show()
```
Thanks @martinfleis for your kind answer. I've tried it, but it looks like the output persists in the old way, without changes in colours or in legend for the second plot
@kerooksblog are you using recent version of geopandas? 0.6.0 or 0.6.1? 
Ops, sorry. I was sure it was the last one. My fault, with the 0.6.1 it works. Sorry and thanks again @martinfleis 

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -448,6 +448,7 @@ def plot_dataframe(
     markersize=None,
     figsize=None,
     legend_kwds=None,
+    categories=None,
     classification_kwds=None,
     missing_kwds=None,
     aspect="auto",
@@ -514,6 +515,8 @@ def plot_dataframe(
     legend_kwds : dict (default None)
         Keyword arguments to pass to matplotlib.pyplot.legend() or
         matplotlib.pyplot.colorbar().
+    categories : list-like
+        Ordered list-like object of categories to be used for categorical plot.
     classification_kwds : dict (default None)
         Keyword arguments to pass to mapclassify
     missing_kwds : dict (default None)
@@ -625,14 +628,35 @@ def plot_dataframe(
 
     nan_idx = pd.isna(values)
 
+    if categories:
+        categorical = True
+
     # Define `values` as a Series
     if categorical:
         if cmap is None:
             cmap = "tab10"
-        categories = list(set(values[~nan_idx]))
-        categories.sort()
+
+        if categories is None:
+            categories = list(set(values[~nan_idx]))
+            categories.sort()
+        else:
+            if not pd.api.types.is_list_like(categories):
+                raise ValueError(
+                    "Categories must be a list-like object. Objects that are "
+                    "considered list-like are for example Python lists, tuples, sets, "
+                    "NumPy arrays, and Pandas Series."
+                )
+            missing = [x for x in values[~nan_idx] if x not in categories]
+            if missing:
+                raise ValueError(
+                    "Column contains values not listed in categories. "
+                    "Missing categories: {}.".format(missing)
+                )
+
         valuemap = dict((k, v) for (v, k) in enumerate(categories))
         values = np.array([valuemap[k] for k in values[~nan_idx]])
+        vmin = 0 if vmin is None else vmin
+        vmax = max(valuemap.values()) if vmax is None else vmax
 
     if scheme is not None:
         if classification_kwds is None:


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
@@ -262,6 +262,33 @@ def test_multipoints_alpha(self):
         with pytest.raises(TypeError):  # no list allowed for alpha
             ax = self.df2.plot(alpha=[0.7, 0.2])
 
+    def test_categorical_list(self):
+        self.df["cats"] = ["cat1", "cat2"] * 5
+        self.df["nums"] = [1, 2] * 5
+        self.df["singlecat"] = ["cat2"] * 10
+        ax1 = self.df.plot("cats", legend=True)
+        ax2 = self.df.plot("singlecat", categories=["cat1", "cat2"], legend=True)
+        ax3 = self.df.plot("nums", categories=[1, 2], legend=True)
+        point_colors1 = ax1.collections[0].get_facecolors()
+        point_colors2 = ax2.collections[0].get_facecolors()
+        point_colors3 = ax3.collections[0].get_facecolors()
+        np.testing.assert_array_equal(
+            point_colors1[1], point_colors2[0], point_colors3[0]
+        )
+        legend1 = [x.get_markerfacecolor() for x in ax1.get_legend().get_lines()]
+        legend2 = [x.get_markerfacecolor() for x in ax2.get_legend().get_lines()]
+        legend3 = [x.get_markerfacecolor() for x in ax3.get_legend().get_lines()]
+        np.testing.assert_array_equal(legend1, legend2)
+        np.testing.assert_array_equal(legend1, legend3)
+
+        with pytest.raises(ValueError, match="Categories must be a list-like object."):
+            self.df.plot(column="cats", categories="non_list")
+
+        with pytest.raises(
+            ValueError, match="Column contains values not listed in categories."
+        ):
+            self.df.plot(column="cats", categories=["cat1"])
+
     def test_misssing(self):
         self.df.loc[0, "values"] = np.nan
         ax = self.df.plot("values")