Repository: geopandas/geopandas

Issue:
CI/COMPAT: TestExplore.test_vmin_vmax failing with latest folium
Failure with latest folium (not yet on main, only on PR with more recent build (https://github.com/geopandas/geopandas/actions/runs/3822563025/jobs/6502838246), so I suspect it the latest commit in folium, potentially https://github.com/python-visualization/folium/pull/1677)

```
__________________________ TestExplore.test_vmin_vmax __________________________
[gw1] linux -- Python 3.10.8 /usr/share/miniconda3/envs/test/bin/python3.10

self = <geopandas.tests.test_explore.TestExplore object at 0x7fcb19d188b0>

    def test_vmin_vmax(self):
        df = self.world.copy()
        df["range"] = range(len(df))
        m = df.explore("range", vmin=-100, vmax=1000)
        out_str = self._fetch_map_string(m)
        assert 'case"176":return{"color":"#3b528b","fillColor":"#3b528b"' in out_str
        assert 'case"119":return{"color":"#414287","fillColor":"#414287"' in out_str
        assert 'case"3":return{"color":"#482173","fillColor":"#482173"' in out_str
    
        # test 0
        df2 = self.nybb.copy()
        df2["values"] = df2["BoroCode"] * 10.0
        m = df2[df2["values"] >= 30].explore("values", vmin=0)
        out_str = self._fetch_map_string(m)
        assert 'case"1":return{"color":"#7ad151","fillColor":"#7ad151"' in out_str
>       assert 'case"2":return{"color":"#22a884","fillColor":"#22a884"' in out_str
E       assert 'case"2":return{"color":"#22a884","fillColor":"#22a884"' in '<!DOCTYPEhtml><html><head><metahttp-equiv="content-type"content="text/html;charset=UTF-8"/><script>L_NO_TOUCH=false;L_DISABLE_3D=false;</script><style>html,body{width:100%;height:100%;margin:0;padding:0;}</style><style>#map{position:absolute;top:0;bottom:0;right:0;left:0;}</style><scriptsrc="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.js"></script><scriptsrc="https://code.jquery.com/jquery-1.12.4.min.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js"></script><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.css"/><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"/><linkrel="stylesheet"href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"/><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.0/css/all.min.css"/><linkrel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css"/><linkrel="stylesheet"href=...b1b9d9f06f3fd18f4.svg=d3.select(".legend.leaflet-control").append("svg").attr("id",\'legend\').attr("width",450).attr("height",40);color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.g=color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.svg.append("g").attr("class","key").attr("transform","translate(25,16)");color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.g.selectAll("rect").data(color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.color.range().map(function(d,i){return{x0:i?color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.x(color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.color.domain()[i-1]):color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.x.range()[0],x1:i<color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.color.domain().length?color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.x(color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.color.domain()[i]):color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.x.range()[1],z:d};})).enter().append("rect").attr("height",40-30).attr("x",function(d){returnd.x0;}).attr("width",function(d){returnd.x1-d.x0;}).style("fill",function(d){returnd.z;});color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.g.call(color_map_670f019f9ce1ce9b1b9d9f06f3fd18f4.xAxis).append("text").attr("class","caption").attr("y",21).text("values");</script></html>'
```

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0235b8..8bd3c4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Development version
 
+New features and improvements:
+
+- Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataframe (#2621).
+
 Deprecations and compatibility notes:
 
 - Added warning that ``unary_union`` will return ``'GEOMETRYCOLLECTION EMPTY'`` instead
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index bf7f4ec..7d7317b 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -86,6 +86,7 @@ Constructive methods and attributes
    GeoSeries.centroid
    GeoSeries.convex_hull
    GeoSeries.envelope
+   GeoSeries.minimum_bounding_circle
    GeoSeries.simplify
    GeoSeries.normalize
 
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index b67cf7e..3640880 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -677,6 +677,18 @@ def representative_point(data):
         return out
 
 
+def minimum_bounding_circle(data):
+    if compat.USE_SHAPELY_20:
+        return shapely.minimum_bounding_circle(data)
+    elif compat.USE_PYGEOS:
+        return pygeos.minimum_bounding_circle(data)
+    else:
+        raise NotImplementedError(
+            f"shapely >= 2.0 or PyGEOS is required, "
+            f"version {shapely.__version__} is installed"
+        )
+
+
 #
 # Binary predicates
 #
diff --git a/geopandas/array.py b/geopandas/array.py
index 5f47233..a80c021 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -511,6 +511,11 @@ class GeometryArray(ExtensionArray):
     def representative_point(self):
         return GeometryArray(vectorized.representative_point(self.data), crs=self.crs)
 
+    def minimum_bounding_circle(self):
+        return GeometryArray(
+            vectorized.minimum_bounding_circle(self.data), crs=self.crs
+        )
+
     def normalize(self):
         return GeometryArray(vectorized.normalize(self.data), crs=self.crs)
 
diff --git a/geopandas/base.py b/geopandas/base.py
index ada4c79..b4c6a90 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -692,6 +692,39 @@ GeometryCollection
         """
         return _delegate_geo_method("representative_point", self)
 
+    def minimum_bounding_circle(self):
+        """Returns a ``GeoSeries`` of geometries representing the minimum bounding
+        circle that encloses each geometry.
+
+        Examples
+        --------
+
+        >>> from shapely.geometry import Polygon, LineString, Point
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
+        ...         LineString([(0, 0), (1, 1), (1, 0)]),
+        ...         Point(0, 0),
+        ...     ]
+        ... )
+        >>> s
+        0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
+        1    LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...
+        2                              POINT (0.00000 0.00000)
+        dtype: geometry
+
+        >>> s.minimum_bounding_circle()
+        0    POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1....
+        1    POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1....
+        2                              POINT (0.00000 0.00000)
+        dtype: geometry
+
+        See also
+        --------
+        GeoSeries.convex_hull : convex hull geometry
+        """
+        return _delegate_geo_method("minimum_bounding_circle", self)
+
     def normalize(self):
         """Returns a ``GeoSeries`` of normalized
         geometries to normal form (or canonical form).


Test patch:
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 3ccd49e..7e4a854 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -14,6 +14,7 @@ import matplotlib.colors as colors  # noqa
 from branca.colormap import StepColormap  # noqa
 
 BRANCA_05 = Version(branca.__version__) > Version("0.4.2")
+FOLIUM_G_014 = Version(folium.__version__) > Version("0.14.0")
 
 
 class TestExplore:
@@ -524,8 +525,14 @@ class TestExplore:
         df2["values"] = df2["BoroCode"] * 10.0
         m = df2[df2["values"] >= 30].explore("values", vmin=0)
         out_str = self._fetch_map_string(m)
-        assert 'case"1":return{"color":"#7ad151","fillColor":"#7ad151"' in out_str
-        assert 'case"2":return{"color":"#22a884","fillColor":"#22a884"' in out_str
+        if FOLIUM_G_014:
+            assert 'case"0":return{"color":"#fde725","fillColor":"#fde725"' in out_str
+            assert 'case"1":return{"color":"#7ad151","fillColor":"#7ad151"' in out_str
+            assert 'default:return{"color":"#22a884","fillColor":"#22a884"' in out_str
+        else:
+            assert 'case"1":return{"color":"#7ad151","fillColor":"#7ad151"' in out_str
+            assert 'case"2":return{"color":"#22a884","fillColor":"#22a884"' in out_str
+            assert 'default:return{"color":"#fde725","fillColor":"#fde725"' in out_str
 
         df2["values_negative"] = df2["BoroCode"] * -10.0
         m = df2[df2["values_negative"] <= 30].explore("values_negative", vmax=0)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 97c1420..eccd5c0 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -928,6 +928,21 @@ class TestGeomMethods:
         assert isinstance(e, GeoSeries)
         assert self.g3.crs == e.crs
 
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="minimum_bounding_circle is only implemented for pygeos, not shapely",
+    )
+    def test_minimum_bounding_circle(self):
+        mbc = self.g1.minimum_bounding_circle()
+        centers = GeoSeries([Point(0.5, 0.5)] * 2)
+        assert np.all(mbc.centroid.geom_equals_exact(centers, 0.001))
+        assert_series_equal(
+            mbc.area,
+            Series([1.560723, 1.560723]),
+        )
+        assert isinstance(mbc, GeoSeries)
+        assert self.g1.crs == mbc.crs
+
     def test_total_bounds(self):
         bbox = self.sol.x, self.sol.y, self.esb.x, self.esb.y
         assert isinstance(self.landmarks.total_bounds, np.ndarray)