Repository: geopandas/geopandas
Pull request #3116 (2023-12-30 10:14:19)

ENH: buffer signature parity with shapely
This is just a minor change aimed at visibility and understandability of all the options `buffer` has. It is all supported via `kwargs` for some time already but explicitly adding keywords to our buffer seems to be better. I am also switching from numbers for join and cap styles to explicit strings (although both options are supported on shapely side). `shapely.BufferCapStyle` and `shapely.BufferJoinStyle` are omitted from the docstring as they are just enums with a limited practical value.

xref #2010


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0a14713..67abca51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ Bug fixes:
   geometry column (#2933).
 - Fix bug in `pandas.concat` CRS consistency checking where CRS differing by WKT
   whitespace only were treated as incompatible (#3023)
+- Fix `explore()` method when the active geometry contains missing and empty geometries (#3094)
 
 ## Version 0.14.1 (Nov 11, 2023)
 
diff --git a/doc/source/_static/code/buffer.py b/doc/source/_static/code/buffer.py
index cba059bf..24d6f539 100644
--- a/doc/source/_static/code/buffer.py
+++ b/doc/source/_static/code/buffer.py
@@ -28,14 +28,14 @@ axs[0, 0].set_title("s.buffer(0.2)")
 s.buffer(0.2, resolution=2).plot(ax=axs[0, 1], alpha=0.6)
 axs[0, 1].set_title("s.buffer(0.2, resolution=2)")
 
-s.buffer(0.2, cap_style=2).plot(ax=axs[1, 0], alpha=0.6)
-axs[1, 0].set_title("s.buffer(0.2, cap_style=2)")
+s.buffer(0.2, cap_style="square").plot(ax=axs[1, 0], alpha=0.6)
+axs[1, 0].set_title('s.buffer(0.2, cap_style="square")')
 
-s.buffer(0.2, cap_style=3).plot(ax=axs[1, 1], alpha=0.6)
-axs[1, 1].set_title("s.buffer(0.2, cap_style=3)")
+s.buffer(0.2, cap_style="flat").plot(ax=axs[1, 1], alpha=0.6)
+axs[1, 1].set_title('s.buffer(0.2, cap_style="flat")')
 
-s.buffer(0.2, join_style=2).plot(ax=axs[2, 0], alpha=0.6)
-axs[2, 0].set_title("s.buffer(0.2, join_style=2)")
+s.buffer(0.2, join_style="mitre").plot(ax=axs[2, 0], alpha=0.6)
+axs[2, 0].set_title('s.buffer(0.2, join_style="mitre")')
 
-s.buffer(0.2, join_style=3).plot(ax=axs[2, 1], alpha=0.6)
-axs[2, 1].set_title("s.buffer(0.2, join_style=3)")
+s.buffer(0.2, join_style="bevel").plot(ax=axs[2, 1], alpha=0.6)
+axs[2, 1].set_title('s.buffer(0.2, join_style="bevel")')
diff --git a/geopandas/array.py b/geopandas/array.py
index b9052514..4017cf15 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -521,7 +521,6 @@ class GeometryArray(ExtensionArray):
         self.check_geographic_crs(stacklevel=5)
         return shapely.length(self._data)
 
-    @property
     def count_coordinates(self):
         out = np.empty(len(self._data), dtype=np.int_)
         out[:] = [shapely.count_coordinates(s) for s in self._data]
diff --git a/geopandas/base.py b/geopandas/base.py
index a2de795f..c2ad701e 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -329,7 +329,6 @@ GeometryCollection
         """
         return _delegate_property("is_empty", self)
 
-    @property
     def count_coordinates(self):
         """
         Returns a ``Series`` containing the count of the number of coordinate pairs
@@ -358,7 +357,7 @@ GeometryCollection
         4                                             None
         dtype: geometry
 
-        >>> s.count_coordinates
+        >>> s.count_coordinates()
         0    4
         1    3
         2    1
@@ -370,7 +369,7 @@ GeometryCollection
         --------
         GeoSeries.get_coordinates : extract coordinates as a :class:`~pandas.DataFrame`
         """
-        return _delegate_property("count_coordinates", self)
+        return Series(self.geometry.values.count_coordinates(), index=self.index)
 
     @property
     def is_simple(self):
@@ -3734,20 +3733,51 @@ GeometryCollection
         """
         return self.geometry.values.has_sindex
 
-    def buffer(self, distance, resolution=16, **kwargs):
+    def buffer(
+        self,
+        distance,
+        resolution=16,
+        cap_style="round",
+        join_style="round",
+        mitre_limit=5.0,
+        single_sided=False,
+        **kwargs,
+    ):
         """Returns a ``GeoSeries`` of geometries representing all points within
         a given ``distance`` of each geometric object.
 
-        See http://shapely.readthedocs.io/en/latest/manual.html#object.buffer
-        for details.
+        Computes the buffer of a geometry for positive and negative buffer distance.
+
+        The buffer of a geometry is defined as the Minkowski sum (or difference, for
+        negative distance) of the geometry with a circle with radius equal to the
+        absolute value of the buffer distance.
+
+        The buffer operation always returns a polygonal result. The negative or
+        zero-distance buffer of lines and points is always empty.
 
         Parameters
         ----------
         distance : float, np.array, pd.Series
-            The radius of the buffer. If np.array or pd.Series are used
-            then it must have same length as the GeoSeries.
+            The radius of the buffer in the Minkowski sum (or difference). If np.array
+            or pd.Series are used then it must have same length as the GeoSeries.
         resolution : int (optional, default 16)
-            The resolution of the buffer around each vertex.
+            The resolution of the buffer around each vertex. Specifies the number of
+            linear segments in a quarter circle in the approximation of circular arcs.
+        cap_style : {'round', 'square', 'flat'}, default 'round'
+            Specifies the shape of buffered line endings. ``'round'`` results in
+            circular line endings (see ``resolution``). Both ``'square'`` and ``'flat'``
+            result in rectangular line endings, ``'flat'`` will end at the original
+            vertex, while ``'square'`` involves adding the buffer width.
+        join_style : {'round', 'mitre', 'bevel'}, default 'round'
+            Specifies the shape of buffered line midpoints. ``'round'`` results in
+            rounded shapes. ``'bevel'`` results in a beveled edge that touches the
+            original vertex. ``'mitre'`` results in a single vertex that is beveled
+            depending on the ``mitre_limit`` parameter.
+        mitre_limit : float, default 5.0
+            Crops of ``'mitre'``-style joins if the point is displaced from the
+            buffered vertex by more than this limit.
+        single_sided : bool, default False
+            Only buffer at one side of the geometry.
 
         Examples
         --------
@@ -3771,13 +3801,12 @@ GeometryCollection
         2    POLYGON ((2.8 -1, 2.8 1, 2.80096 1.0196, 2.803...
         dtype: geometry
 
-        ``**kwargs`` accept further specification as ``join_style`` and ``cap_style``.
-        See the following illustration of different options.
+        ``Further specification as ``join_style`` and ``cap_style`` are shown in the
+        following illustration:
 
         .. plot:: _static/code/buffer.py
 
         """
-        # TODO: update docstring based on pygeos after shapely 2.0
         if isinstance(distance, pd.Series):
             if not self.index.equals(distance.index):
                 raise ValueError(
@@ -3787,7 +3816,15 @@ GeometryCollection
             distance = np.asarray(distance)
 
         return _delegate_geo_method(
-            "buffer", self, distance, resolution=resolution, **kwargs
+            "buffer",
+            self,
+            distance,
+            resolution=resolution,
+            cap_style=cap_style,
+            join_style=join_style,
+            mitre_limit=mitre_limit,
+            single_sided=single_sided,
+            **kwargs,
         )
 
     def simplify(self, *args, **kwargs):
diff --git a/geopandas/explore.py b/geopandas/explore.py
index 49509712..c01d313a 100644
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -618,7 +618,9 @@ def _explore(
         tooltip = None
         popup = None
     # escape the curly braces {{}} for jinja2 templates
-    feature_collection = gdf.__geo_interface__
+    feature_collection = gdf[
+        ~(gdf.geometry.isna() | gdf.geometry.is_empty)  # drop missing or empty geoms
+    ].__geo_interface__
     for feature in feature_collection["features"]:
         for k in feature["properties"]:
             # escape the curly braces in values


Test patch:
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index dba61184..9e134a7b 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -2,6 +2,7 @@ import geopandas as gpd
 import numpy as np
 import pandas as pd
 import pytest
+import shapely
 from packaging.version import Version
 
 folium = pytest.importorskip("folium")
@@ -940,3 +941,17 @@ class TestExplore:
                     "zoom_control": False,
                 }
             )
+
+    def test_none_geometry(self):
+        # None and empty geoms are dropped prior plotting
+        df = self.nybb.copy()
+        df.loc[0, df.geometry.name] = None
+        m = df.explore()
+        self._fetch_map_string(m)
+
+    def test_empty_geometry(self):
+        # None and empty geoms are dropped prior plotting
+        df = self.nybb.copy()
+        df.loc[0, df.geometry.name] = shapely.Point()
+        m = df.explore()
+        self._fetch_map_string(m)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 26d440c0..0c0cd347 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -499,10 +499,10 @@ class TestGeomMethods:
 
     def test_count_coordinates(self):
         expected = Series(np.array([4, 5]), index=self.g1.index)
-        self._test_unary_real("count_coordinates", expected, self.g1)
+        assert_series_equal(self.g1.count_coordinates(), expected)
 
         expected = Series(np.array([4, 0]), index=self.na_none.index)
-        self._test_unary_real("count_coordinates", expected, self.na_none)
+        assert_series_equal(self.na_none.count_coordinates(), expected)
 
     def test_crosses(self):
         expected = [False, False, False, False, False, False, False]