Repository: geopandas/geopandas

Issue:
ENH: geopandas explore() ability to customise style_function with geojson attributes
#### Is your feature request related to a problem?

`explore()` provide **style_kwds** to pass constants to **style_function**.  There is no ability to change behaviour of **style_function** to use *geojson* properties

 
#### Describe the solution you'd like

Extend **style_kwds** parameter such that *geojson* properties can be passed and used in internally created style functions.

#### API breaking implications

None

#### Describe alternatives you've considered

Have used monkey patching to work around.  Details in python section.

#### Additional context

Would like additional input, would like input ideas before creating a PR to implement a solution to this enhancement

```python
import io
import pandas as pd
import geopandas as gpd
import shapely
import folium
import wrapt

df = pd.read_csv(
    io.StringIO(
        """|             geometry        | count |
|   POINT (6.92334 50.91695)  |   10  |
|   POINT (6.91970 50.93167)  |    8  |
|   POINT (6.96946 50.91469)  |    2  |
|   POINT (6.96266 50.94713)  |   72  |
|   POINT (6.93751 50.93832)  |  267  |"""
    ),
    sep="|",
).pipe(
    lambda d: d.drop(columns=[c for c in d.columns if "Unnamed" in c]).rename(
        columns={c: c.strip() for c in d.columns}
    )
)

# wanna intercept geopandas.explore() style function so can set size too
@wrapt.patch_function_wrapper(folium, "GeoJson")
def new_style(wrapped, instance, args, kwargs):
    def style_fn(x):
        return {
            "fillColor": x["properties"]["__folium_color"],
            "color": x["properties"]["__folium_color"],
            "radius": x["properties"]["size"],
            "fillOpacity":.8
        }

    if "_style_column" in str(kwargs["style_function"]):
        kwargs["style_function"] = style_fn
    return wrapped(*args, **kwargs)


gdf = gpd.GeoDataFrame(
    df, geometry=df["geometry"].apply(shapely.wkt.loads), crs="epsg:4326"
)
# normalize sizes...
gdf["size"] = gdf["count"].rank() * 20

gdf.explore(cmap="Reds", column="count")


```




Discussion:
Hi, thanks for the suggestion! This sounds reasonable, would you be willing to try a PR enabling it?
created https://github.com/geopandas/geopandas/pull/2354

Resolving patch:
diff --git a/geopandas/explore.py b/geopandas/explore.py
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -183,6 +183,16 @@ def my_colormap(value):  # scalar value defined in 'column'
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoDataFrame.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -444,6 +454,17 @@ def my_colormap(value):  # scalar value defined in 'column'
         style_kwds["fillOpacity"] = 0.5
     if "weight" not in style_kwds:
         style_kwds["weight"] = 2
+    if "style_function" in style_kwds:
+        style_kwds_function = style_kwds["style_function"]
+        if not callable(style_kwds_function):
+            raise ValueError("'style_function' has to be a callable")
+        style_kwds.pop("style_function")
+    else:
+
+        def _no_style(x):
+            return {}
+
+        style_kwds_function = _no_style
 
     # specify color
     if color is not None:
@@ -454,10 +475,14 @@ def my_colormap(value):  # scalar value defined in 'column'
         ):  # use existing column
 
             def _style_color(x):
-                return {
+                base_style = {
                     "fillColor": x["properties"][color],
                     **style_kwds,
                 }
+                return {
+                    **base_style,
+                    **style_kwds_function(x),
+                }
 
             style_function = _style_color
         else:  # assign new column
@@ -476,27 +501,35 @@ def _style_color(x):
             if not stroke_color:
 
                 def _style_column(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": x["properties"]["__folium_color"],
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_column
             else:
 
                 def _style_stroke(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": stroke_color,
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_stroke
     else:  # use folium default
 
         def _style_default(x):
-            return {**style_kwds}
+            return {**style_kwds, **style_kwds_function(x)}
 
         style_function = _style_default
 
@@ -810,6 +843,7 @@ def _explore_geoseries(
     marker_kwds={},
     style_kwds={},
     highlight_kwds={},
+    map_kwds={},
     **kwargs,
 ):
     """Interactive map based on GeoPandas and folium/leaflet.js
@@ -880,6 +914,17 @@ def _explore_geoseries(
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoSeries.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
+
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -887,6 +932,9 @@ def _explore_geoseries(
     highlight_kwds : dict (default {})
         Style to be passed to folium highlight_function. Uses the same keywords
         as ``style_kwds``. When empty, defaults to ``{"fillOpacity": 0.75}``.
+    map_kwds : dict (default {})
+        Additional keywords to be passed to folium :class:`~folium.folium.Map`,
+        e.g. ``dragging``, or ``scrollWheelZoom``.
 
     **kwargs : dict
         Additional options to be passed on to the folium.
@@ -911,5 +959,6 @@ def _explore_geoseries(
         marker_kwds=marker_kwds,
         style_kwds=style_kwds,
         highlight_kwds=highlight_kwds,
+        map_kwds=map_kwds,
         **kwargs,
     )


Test patch:
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -309,6 +309,37 @@ def test_style_kwds(self):
         m = self.world.explore(column="pop_est", style_kwds=dict(color="black"))
         assert '"color":"black"' in self._fetch_map_string(m)
 
+        # custom style_function - geopandas/issues/2350
+        m = self.world.explore(
+            style_kwds={
+                "style_function": lambda x: {
+                    "fillColor": "red"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "green",
+                    "color": "black"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "white",
+                }
+            }
+        )
+        # two lines with formatting instructions from style_function.
+        # make sure each passes test
+        assert all(
+            [
+                ('"fillColor":"green"' in t and '"color":"white"' in t)
+                or ('"fillColor":"red"' in t and '"color":"black"' in t)
+                for t in [
+                    "".join(line.split())
+                    for line in m._parent.render().split("\n")
+                    if "return" in line and "color" in line
+                ]
+            ]
+        )
+
+        # style function has to be callable
+        with pytest.raises(ValueError, match="'style_function' has to be a callable"):
+            self.world.explore(style_kwds={"style_function": "not callable"})
+
     def test_tooltip(self):
         """Test tooltip"""
         # default with no tooltip or popup