Repository: geopandas/geopandas

Issue:
Vendoring descartes? (or take over functionality)
Triggered by https://github.com/conda-forge/geopandas-feedstock/pull/59, where matplotlib and descartes are removed from the packages that are automatically installed for geopandas (note: only for conda of course, not for pip), I was wondering if it would be an idea to vendor descartes. In the end, it is less than 100 lines of code and rather stable (not much changes over the last years).

Reason I was thinking this: people installing geopandas will quite often have matplotlib installed as well. But, plotting (polygons) still does not work out of the box in such a case, for that people have to install descartes as well. 
It would be nice if plotting "just works" (in case if matplotlib is already installed of course).

On the other hand, doing a `conda/pip install descartes` is also not that hard (it's an easy pure python package to install), certainly if we give a good error message. 
And you could say the same for mapclassify ..

cc @sgillies 




Discussion:
We should at least update the installation notes (https://geopandas.readthedocs.io/en/latest/install.html) to make clearly mention how to install "essential" optional packages as well.
👍 to vendoring or copying descartes. I don't have any plans to make another release of that project or keep it up to date with future versions of matplotlib, so bringing it inside geopandas could remove a fragile dependency.
@sgillies Thanks for that feedback, that's good to know

cc @ResidentMario might be relevant for you as well
@jorisvandenbossche that's :+1: with me.
Another option would also be to take over maintenance (if that is something that @sgillies would be fine with)

@ResidentMario the "that's fine with me", was that towards the "vendoring descartes in geopandas" ? But, would you in such a case want to rely on the functionality for that from geopandas instead of relying on descartes (for the latest versions). 
If we are both using it, it would be good to have a single maintained version. So that could be included in geopandas, but as above, we could also keep it in descartes if we maintain it ourselves.

If we want to take over, does it make sense to keep it as a separate package if it is just 100 lines? I would vote for vendoring it in geopandas. It just complicates maintenance.
`npm` has plenty of code packages that are 100 lines or less. There's less of a tradition of this in Python, but `descartes` does seem to me to be a package whose size crosses the threshold of having a feature-set large enough and complex enough to warrant being its own package.

Vendoring `descartes` would mean adding this functionality to `geopandas`, which would mean expanding the scope of the project somewhat. It would also mean that other projects that rely on `descartes`, the Python package, would fall behind the geopandas-vendored version of the library in code recency. And there's [a nontrivial amount of such projects](https://github.com/search?l=Python&o=desc&q=%22import+descartes%22&s=indexed&type=Code).

Overall I'm fine with either (1) vendoring it or (2) transferring ownership to e.g. `geopandas/descartes` and maintaining it as part of the `geopandas` org. But preference for the latter.
> adding this functionality to geopandas, which would mean expanding the scope of the project somewhat.

I think the expansion of scope is quite OK (we already have quite some code for handling the plotting). Looking at the descartes code again, and factoring away the code in there that is only needed for handling non-shapely geometries or other geom types, the code we need is basically:

```
from matplotlib.patches import PathPatch
from matplotlib.path import Path
from numpy import asarray, concatenate, ones


def PolygonPatch(polygon, **kwargs):

    def coding(ob):
        # The codes will be all "LINETO" commands, except for "MOVETO"s at the
        # beginning of each subpath
        n = len(getattr(ob, 'coords', None) or ob)
        vals = np.ones(n, dtype=Path.code_type) * Path.LINETO
        vals[0] = Path.MOVETO
        return vals

    vertices = np.concatenate(
        [np.asarray(polygon.exterior)[:, :2]] +
        [np.asarray(r)[:, :2] for r in polygon.interiors])
    codes = np.concatenate(
        [coding(polygon.exterior)] + [coding(r) for r in polygon.interiors])

    return PathPatch(Path(vertices, codes), **kwargs)
```

So if we include this functionality in geopandas, I would probably not even "vendor" descartes, but just include an adapted version of that in `plotting.py`.
hey colleagues @jorisvandenbossche @martinfleis did you guys decide to vendor descartes? i'm asking because we ran into the issue in one of our packages of needing descartes and i'm trying to decide how to handle this dependency / separate install? What do you guys recommend? should we make it a dependency? we are essentially [testing geopandas plots](https://github.com/earthlab/matplotcheck/pull/188) and we test polygons! 
many thanks for any suggestions. 

@nkorinek just a note that i'm exploring options here for matplotcheck !! 
If the full code really is just that snippet, this seems like something that can just go upstream in Matplotlib (but probably as a PathPatch sub-class rather than a camel-case function).  While we are trying to avoid growing the scope of Matplotlib, a 15 line class that supports a common data structure does not seem to be too heavy a of a lift to me.

I don't think we would want to add new Axes / pyplot API for it, just the class.

Could that code be pushed into shapely?
Getting back to this. After seeing the discussion in https://github.com/matplotlib/matplotlib/issues/16662 it seems that the best solution would be to include that snippet above in geopandas.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -94,6 +94,32 @@ def _expand_kwargs(kwargs, multiindex):
             kwargs[att] = np.take(value, multiindex, axis=0)
 
 
+def _PolygonPatch(polygon, **kwargs):
+    """Constructs a matplotlib patch from a Polygon geometry
+
+    The `kwargs` are those supported by the matplotlib.patches.PathPatch class
+    constructor. Returns an instance of matplotlib.patches.PathPatch.
+
+    Example (using Shapely Point and a matplotlib axes)::
+
+        b = shapely.geometry.Point(0, 0).buffer(1.0)
+        patch = _PolygonPatch(b, fc='blue', ec='blue', alpha=0.5)
+        ax.add_patch(patch)
+
+    GeoPandas originally relied on the descartes package by Sean Gillies
+    (BSD license, https://pypi.org/project/descartes) for PolygonPatch, but
+    this dependency was removed in favor of the below matplotlib code.
+    """
+    from matplotlib.patches import PathPatch
+    from matplotlib.path import Path
+
+    path = Path.make_compound_path(
+        Path(np.asarray(polygon.exterior.coords)[:, :2]),
+        *[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors]
+    )
+    return PathPatch(path, **kwargs)
+
+
 def _plot_polygon_collection(
     ax, geoms, values=None, color=None, cmap=None, vmin=None, vmax=None, **kwargs
 ):
@@ -123,15 +149,6 @@ def _plot_polygon_collection(
     -------
     collection : matplotlib.collections.Collection that was plotted
     """
-
-    try:
-        from descartes.patch import PolygonPatch
-    except ImportError:
-        raise ImportError(
-            "The descartes package is required for plotting polygons in geopandas. "
-            "You can install it using 'conda install -c conda-forge descartes' or "
-            "'pip install descartes'."
-        )
     from matplotlib.collections import PatchCollection
 
     geoms, multiindex = _flatten_multi_geoms(geoms)
@@ -152,7 +169,7 @@ def _plot_polygon_collection(
     _expand_kwargs(kwargs, multiindex)
 
     collection = PatchCollection(
-        [PolygonPatch(poly) for poly in geoms if not poly.is_empty], **kwargs
+        [_PolygonPatch(poly) for poly in geoms if not poly.is_empty], **kwargs
     )
 
     if values is not None:


Test patch:
diff --git a/geopandas/tests/test_api.py b/geopandas/tests/test_api.py
--- a/geopandas/tests/test_api.py
+++ b/geopandas/tests/test_api.py
@@ -12,7 +12,6 @@ def test_no_additional_imports():
         "py",
         "ipython",
         # "matplotlib",  # matplotlib gets imported by pandas, see below
-        "descartes",
         "mapclassify",
         # 'rtree',  # rtree actually gets imported if installed
         "sqlalchemy",
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
@@ -1469,6 +1469,22 @@ def test_column_values():
         ax = df.plot(column=np.array([1, 2, 3]))
 
 
+def test_polygon_patch():
+    # test adapted from descartes by Sean Gillies
+    # (BSD license, https://pypi.org/project/descartes).
+    from geopandas.plotting import _PolygonPatch
+    from matplotlib.patches import PathPatch
+
+    polygon = (
+        Point(0, 0).buffer(10.0).difference(MultiPoint([(-5, 0), (5, 0)]).buffer(3.0))
+    )
+
+    patch = _PolygonPatch(polygon)
+    assert isinstance(patch, PathPatch)
+    path = patch.get_path()
+    assert len(path.vertices) == len(path.codes) == 198
+
+
 def _check_colors(N, actual_colors, expected_colors, alpha=None):
     """
     Asserts that the members of `collection` match the `expected_colors`