Efficient filtering of GeoJSON components with python dash-leaflet

Viewed 1423

I'm using the Dash Leaflet map component dash-leaflet for my interactive map visualizations.

My goal is to filter large GeoJSON components (dl.GeoJSON) by values of dash components (e.g. dcc.Slider).

My current approach is the following:

import random
import dash
import dash_html_components as html
import dash_leaflet as dl
import dash_leaflet.express as dlx
import dash_core_components as dcc
from dash.dependencies import Input, Output

# Create some markers.
points = [dict(lat=55.5 + random.random(), lon=9.5 + random.random(), value=random.random()*100) for i in range(100)]
data = dlx.dicts_to_geojson(points)

app = dash.Dash()
app.layout = html.Div([
    dl.Map([
        dl.TileLayer(),
        dl.GeoJSON(id="data-id", data=data)
        ], center=(56, 10), zoom=8, style={'height': '50vh'}),
    html.Div([
        html.H5('Filtering'),
        dcc.Slider(id='my-slider', min=0, max=100, step=1, value=100),
        html.Div(id='slider-output-container')
        ], style={'width': '30%'}),
    ])

@app.callback(
    Output('slider-output-container', 'children'),
    Output('data-id', 'data'),
    [Input('my-slider', 'value')])
def update_output(value):
    points_new = [p for p in points if p['value'] <= value]
    data_new = dlx.dicts_to_geojson(points_new)
    return 'You have selected value "{}"'.format(value), data_new

if __name__ == '__main__':
    app.run_server()

In this example, the data object of the GeoJSON component "data-id" is filtered with the value of a dash component "my-slider" by filtering the list of points depending on the input value and returning a new geoJSON object created with the dlx.dicts_to_geojson function:

points_new = [p for p in points if p['value'] <= value]
data_new = dlx.dicts_to_geojson(points_new)

Is this the right way to filter a geoJSON object?

I can imagine that there exists better ways to define such a filter function on the client side with the options-feature of the GeoJSON component and a javascript function but I don't know how.

I appreciate any advice/code example I can get.

1 Answers

While it is possible to filter the data in Python, it can introduce significant network overhead depending on the data size (the data is transferred from the server to the client each time the filter changes). If you do the filtering client side, you only have to transfer the data once, i.e. the performance difference can be dramatic.

The client side filtering can be implemented by adding a JavaScript asset (i.e. a .js file placed in the assets folder) with the filtering function as per the documentation,

window.myNamespace = Object.assign({}, window.myNamespace, {  
    mySubNamespace: {  
        filter_features: function(feature, context) {
            // code should return true if feature is included, otherwise false
            const value = context.props.hideout['value']
            ...
        }  
    }  
});

To apply the filtering, pass a function handle to the filter function to the GeoJSON component,

import dash_leaflet as dl
from dash_extensions.javascript import Namespace
...
ns = Namespace("myNamespace ", "mySubNamespace")
dl.GeoJSON(id="geojson", options=dict(filter=ns("filter_features"), ...)

Finally, by making the filter dependent of the hideout prop (as indicated in the example code above), you can achieve interactivity by updating this prop via a callback,

@app.callback(Output("geojson", "hideout"), ...)
def update(...):
    ...
    return {"value": value}

EDIT: As per request in the comments, here is a small self-contained example demonstrating interactive geojson filtering using only client side logic,

import dash_html_components as html
import dash_leaflet as dl
import dash_core_components as dcc
import dash_leaflet.express as dlx
from dash import Dash
from dash.dependencies import Output, Input
from dash_extensions.javascript import assign

# A few cities in Denmark.
cities = [dict(name="Aalborg", lat=57.0268172, lon=9.837735),
          dict(name="Aarhus", lat=56.1780842, lon=10.1119354),
          dict(name="Copenhagen", lat=55.6712474, lon=12.5237848)]
# Create drop down options.
dd_options = [dict(value=c["name"], label=c["name"]) for c in cities]
dd_defaults = [o["value"] for o in dd_options]
# Generate geojson with a maker for each city and name as tooltip.
geojson = dlx.dicts_to_geojson([{**c, **dict(tooltip=c['name'])} for c in cities])
# Create javascript function that filters on feature name.
geojson_filter = assign("function(feature, context){return context.props.hideout.includes(feature.properties.name);}")
# Create example app.
app = Dash()
app.layout = html.Div([
    dl.Map(children=[
        dl.TileLayer(),
        dl.GeoJSON(data=geojson, options=dict(filter=geojson_filter), hideout=dd_defaults, id="geojson")
    ], style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"}, id="map"),
    dcc.Dropdown(id="dd", value=dd_defaults, options=dd_options, clearable=False, multi=True)
])
# Link drop down to geojson hideout prop (could also be done with a normal callback).
app.clientside_callback("function(x){return x;}", Output("geojson", "hideout"), Input("dd", "value"))

if __name__ == '__main__':
    app.run_server()

Note that it requires dash-extensions==0.0.55.

Related