Repository: geopandas/geopandas
Pull request #3094 (2023-12-04 11:07:10)

BUG: allow None and empty geoms in explore
As pointed out by @anitagraser on Mastodon, `explore()` raises an error when there is a missing or an empty geometry in the active column. This drops affected rows prior generating a JSON for Folium, as there's nothing to show.

martinfleis: The generation of the HTML is lazy, so if you call `explore` inside tests, nearly nothing is actually tested. Surely not this case. 
m-richards: > The generation of the HTML is lazy, so if you call `explore` inside tests, nearly nothing is actually tested. Surely not this case.

Ah yes you're right, it's the rendering which triggers the crash, separate to the `.explore()`

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/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..ccd9b10f 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):
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]