Dash Leaflet: How do I implement clientside filtering of a GeoJSON when clustering is enabled?

Lots of talk about using clientside callbacks to filter a geoJSON: Dash-leaflet - Efficiency filtering a large GeoJson (11 mb) - #2 by Emil

And of course the official docs:
https://www.dash-leaflet.com/docs/geojson_tutorial#a-interactivity-via-the-hideout-prop

My issue is that I cannot set cluster=true in dl.GeoJSON while also using the hideout + clientside callbacks method because the callback/filter won’t update the clusters. The individual points do appear or disappear depending on the filter, which is great, but the clusters themselves remain unchanged. If I click into a cluster and play with a dcc.RangeSlider for example, the individual nodes appear and disappear… but other clusters in the viewport don’t change at all.

I’m unable to find any working examples of using clientside callbacks & filtering while using cluster=True.

Anyone have any ideas?

This is how I have filter working and cluster.

I have a dmc.Select within along with a dmc.TextInput for type to search.

I’m using a custom geojson script for the pointToLayer which can allow you to change the markers on zoom to whatever you’d like and the clusterToLayer prop allows you to change the cluster bubble design but basically within my dl.Map I have:

dl.GeoJSON(
      data=geojson_dict,
      id="locations_layer",
      cluster=True,
      zoomToBoundsOnClick=True,
      superClusterOptions=dict(radius=40),
      hideout=dict(
          m_type_colors=m_type_colors,
          circleOptions=dict(fillOpacity=0.5, stroke=False, radius=3),
          min=0,
      ),
      pointToLayer=point_to_layer_js,
      clusterToLayer=cluster_to_layer,
  )

Most of the magic happens within the callback for filtering a package that I use that would be helpful would be geopandas which can help you translate a dataframe into a geopandas that will work in the filter process for example my callback looks like:

@callback(
    Output("quick-filter-simple", "dashGridOptions"),
    Output("locations_layer", "data", allow_duplicate=True),
    Output("quick-filter-simple", "rowData", allow_duplicate=True),
    Input("quick-filter-input", "value"),
    Input("location_type", "value"),
    Input('autocomplete_r_map', 'value'),
    prevent_initial_call=True,
)
def update_filter_and_locations_layer(filter_value, location_type, city):
    # Filter by city
    if city == "Everything":
        rowData = everything_df
    else:
        rowData = everything_df[everything_df['city'] == city]

    # Filter by location type
    if location_type != "everything":
        rowData = rowData[rowData['type'] == location_type]

    # Filter by name
    if filter_value:
        rowData = rowData[rowData['name'].str.contains(f'{filter_value}', case=False, na=False)]

    # Convert the filtered DataFrame to GeoJSON
    gdf = gpd.GeoDataFrame(
        rowData, geometry=gpd.points_from_xy(rowData.lon, rowData.lat))

    geojson = gdf.to_json()
    geojson_dict = json.loads(geojson)

    # Update the quick filter
    newFilter = Patch()
    newFilter["quickFilterText"] = filter_value

    return newFilter, geojson_dict, rowData.to_dict("records")

Hope this gives some clues to help you figure out a working example for your data and use case.

It shouldn’t matter if the callback is clients use or not. To me, it sounds like a bug, i.e. that the cluster data structure is not correctly updated when the filter is applied. If this is the case, a fix in dash-leaflet would be the best way forward.