codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=h1) Report
> Merging [#1173](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/74ff43c38674bf8864c0e44a26ca9edaa20bcdbd?src=pr&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/geopandas/geopandas/pull/1173/graphs/tree.svg?width=650&token=6TOPMcCBhB&height=150&src=pr)](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1173      +/-   ##
==========================================
+ Coverage   90.19%   90.26%   +0.07%     
==========================================
  Files          21       21              
  Lines        2039     2044       +5     
==========================================
+ Hits         1839     1845       +6     
+ Misses        200      199       -1
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/plotting.py](https://codecov.io/gh/geopandas/geopandas/pull/1173/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Bsb3R0aW5nLnB5) | `93.84% <100%> (+0.51%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=footer). Last update [74ff43c...9fe7085](https://codecov.io/gh/geopandas/geopandas/pull/1173?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

jorisvandenbossche: Sorry for the slow response on the issues/PRs, but, I was thinking: an alternative solution could also be to actually support categorical column values better? Eg try to fix that we follow the order of the categorical values (and in that way, you can also have categorical values with more categories than are present in your data, for that use case).
martinfleis: @jorisvandenbossche not sure if I understand correctly. You mean changing the default behaviour to return the order of categories as they appear in the df instead of alphabetical sorting? On top of this PR?
jorisvandenbossche: I mean specific handling of columns of categorical dtype. Such columns have a fixed set of categories and order defined, which we currently loose due to how we convert them.
martinfleis: That is true, but even then I feel that having simple way how to pass categories is useful. Sometimes you might want to make categorical plot of numerical data, with specific order or additional categories and forcing users to make categorical column in that case seems to be too much. 

But I agree that we should support categorical dtype for categorical plots, but I would open specific issue about that and make another PR allowing both solutions.
jorisvandenbossche: @martinfleis what happens if there is a category in the data that is not listed in the list passed to `categories` ?

Also, passing `categories` should maybe automatically trigger `categorical=True` ?
martinfleis: > @martinfleis what happens if there is a category in the data that is not listed in the list passed to categories ?

I've added a check for that now raising ValueError.

> Also, passing categories should maybe automatically trigger categorical=True ?

True. Done.
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 9076113b..169fe39d 100644
--- 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:

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)

diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 2baf53f5..2dec35ed 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -262,6 +262,33 @@ class TestPointPlotting:
         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")

pip install -e .
parse_log_pytest
requirements.txt
pytest descartes matplotlib rtree
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==24.2.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
click==8.1.8
click-plugins==1.1.1
cligj==0.7.2
cycler==0.11.0
descartes==1.1.0
exceptiongroup==1.2.2
fiona==1.9.6
fonttools==4.38.0
-e git+https://github.com/geopandas/geopandas.git@bbf06a753c0b8f96fc4c4e0286ec9d55062345c5#egg=geopandas
importlib-metadata==6.7.0
iniconfig==2.0.0
kiwisolver==1.4.5
matplotlib==3.5.3
numpy==1.21.6
packaging==24.0
pandas==1.3.5
Pillow==9.5.0
pluggy==1.2.0
pyparsing==3.1.4
pyproj==3.2.1
pytest==7.4.4
python-dateutil==2.9.0.post0
pytz==2025.2
Rtree==1.0.1
shapely==2.0.7
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
zipp==3.15.0

name: geopandas
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==24.2.0
      - click==8.1.8
      - click-plugins==1.1.1
      - cligj==0.7.2
      - cycler==0.11.0
      - descartes==1.1.0
      - exceptiongroup==1.2.2
      - fiona==1.9.6
      - fonttools==4.38.0
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - kiwisolver==1.4.5
      - matplotlib==3.5.3
      - numpy==1.21.6
      - packaging==24.0
      - pandas==1.3.5
      - pillow==9.5.0
      - pluggy==1.2.0
      - pyparsing==3.1.4
      - pyproj==3.2.1
      - pytest==7.4.4
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - rtree==1.0.1
      - shapely==2.0.7
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/geopandas

geopandas/tests/test_plotting.py::TestPointPlotting::test_categorical_list
geopandas/tests/test_plotting.py::TestPointPlotting::test_markerstyle geopandas/tests/test_plotting.py::TestPointPlotting::test_style_kwargs_alpha geopandas/tests/test_plotting.py::TestPointPlotting::test_legend geopandas/tests/test_plotting.py::TestPointPlotting::test_multipoints geopandas/tests/test_plotting.py::TestPointPlotting::test_multipoints_alpha geopandas/tests/test_plotting.py::TestLineStringPlotting::test_single_color geopandas/tests/test_plotting.py::TestLineStringPlotting::test_style_kwargs_linestyle geopandas/tests/test_plotting.py::TestLineStringPlotting::test_style_kwargs_linewidth geopandas/tests/test_plotting.py::TestLineStringPlotting::test_style_kwargs_alpha geopandas/tests/test_plotting.py::TestLineStringPlotting::test_subplots_norm geopandas/tests/test_plotting.py::TestLineStringPlotting::test_multilinestrings geopandas/tests/test_plotting.py::TestPolygonPlotting::test_single_color geopandas/tests/test_plotting.py::TestPolygonPlotting::test_vmin_vmax geopandas/tests/test_plotting.py::TestPolygonPlotting::test_style_kwargs_color geopandas/tests/test_plotting.py::TestPolygonPlotting::test_style_kwargs_linestyle geopandas/tests/test_plotting.py::TestPolygonPlotting::test_style_kwargs_linewidth geopandas/tests/test_plotting.py::TestPolygonPlotting::test_legend_kwargs geopandas/tests/test_plotting.py::TestPolygonPlotting::test_colorbar_kwargs geopandas/tests/test_plotting.py::TestPolygonPlotting::test_multipolygons_color geopandas/tests/test_plotting.py::TestPolygonPlotting::test_multipolygons_linestyle geopandas/tests/test_plotting.py::TestPolygonPlotting::test_multipolygons_linewidth geopandas/tests/test_plotting.py::TestPolygonPlotting::test_multipolygons_alpha geopandas/tests/test_plotting.py::TestPolygonPlotting::test_subplots_norm geopandas/tests/test_plotting.py::TestPolygonZPlotting::test_plot geopandas/tests/test_plotting.py::TestGeometryCollectionPlotting::test_colors geopandas/tests/test_plotting.py::TestGeometryCollectionPlotting::test_values geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_colors geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_style_kwargs geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_style_kwargs_linestyle geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_style_kwargs_linewidth geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_style_kwargs_alpha geopandas/tests/test_plotting.py::TestGeographicAspect::test_auto geopandas/tests/test_plotting.py::TestGeographicAspect::test_manual geopandas/tests/test_plotting.py::TestPlotCollections::test_linestrings geopandas/tests/test_plotting.py::TestPlotCollections::test_linestrings_values geopandas/tests/test_plotting.py::TestPlotCollections::test_polygons geopandas/tests/test_plotting.py::TestPlotCollections::test_polygons_values geopandas/tests/test_plotting.py::test_column_values
geopandas/tests/test_plotting.py::TestPointPlotting::test_figsize geopandas/tests/test_plotting.py::TestPointPlotting::test_default_colors geopandas/tests/test_plotting.py::TestPointPlotting::test_colormap geopandas/tests/test_plotting.py::TestPointPlotting::test_single_color geopandas/tests/test_plotting.py::TestPointPlotting::test_markersize geopandas/tests/test_plotting.py::TestPointPlotting::test_style_kwargs geopandas/tests/test_plotting.py::TestPointPlotting::test_subplots_norm geopandas/tests/test_plotting.py::TestPointPlotting::test_empty_plot geopandas/tests/test_plotting.py::TestPointPlotting::test_misssing geopandas/tests/test_plotting.py::TestPointZPlotting::test_plot geopandas/tests/test_plotting.py::TestPlotCollections::test_points geopandas/tests/test_plotting.py::TestPlotCollections::test_points_values
geopandas/plotting.py
geopandas/plotting.py