Adding interactivity to dash_leflet map

In this dash_leaflet app if I clicked on certain governorate, its color will change to highlight that I’ve selected it as the following image:

I’m trying to make the dropdown do the same effect. If I select a governorate from the dropdown it will highlight it. I can’t get it done.
Here’s my code so far:

import random, json
import dash
from dash import dcc, html, Dash, callback, Output, Input, State
import dash_leaflet as dl
import geopandas as gpd
from dash_extensions.javascript import assign
import pandas as pd

df = pd.read_csv('data/Eastren_region_indicators.csv')
#df = df.sort_values('Year')
app = dash.Dash(__name__)
# distinct_governorates = df['Governorates'].unique()
gdf = gpd.read_file("data/OutputEPSG4326.geojson")
gdf['tooltip'] = gdf['GOVERNORATENAME_EN']
distinct_governorates = gdf['GOVERNORATENAME_EN'].unique()
style_handle = assign("""function (feature, context) {
    const selection = context.props.hideout || {};
    if (feature.id in selection) {
        return {color: '#AA4A44', fillColor: '#AA4A44', weight: 2};
    }
    return {color: '#333', fillColor: '#f5f0e4', weight: 1};
}""")


app.layout = html.Div([
    dl.Map([
        dl.TileLayer(url="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png"),
        dl.GeoJSON(data=json.loads(gdf.to_json()), id="state-layer",
                   options=dict(style=style_handle))],
        style={'width': '100%', 'height': '600px'},
        id="map",
        center=[24, 47.5],
    ),
dcc.Dropdown(
                id='search-bar',
                options=[{'label': gov, 'value': gov} for gov in distinct_governorates],
                placeholder='Search Governorates...',
                clearable=True,
            )
])

app.clientside_callback(
    """
    function(clickedFeature, hideout) {
        if (!clickedFeature) {
            // NB. raise PreventUpdate to prevent ALL outputs updating, 
            // dash.no_update prevents only a single output updating
            throw window.dash_clientside.PreventUpdate;
        }

        const id = clickedFeature.id;
        const selection = hideout || {};

        if (id in selection) {
            delete selection[id];
        }
        else {
            selection[id] = clickedFeature;
        }

        return [selection, null];
    }
    """,
    Output("state-layer", "hideout"),
    Output("state-layer", "click_feature"),
    Input("state-layer", "click_feature"),
)

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

The example doesn’t run without the data, so I can’t debug exactly what goes wrong in your case. Instead, I have created a small example that demonstrates how to achieve the desired functionality using a different dataset,

import json
import dash_leaflet as dl
from urllib.request import urlopen
from dash import Dash, dcc, html, Output, Input
from dash_extensions.javascript import assign

url = "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json"
# Color selected state(s) red.
style_handle = assign("""function(feature, context){
    const {selected} = context.props.hideout;
    if(selected.includes(feature.properties.name)){
        return {fillColor: 'red'}
    }
}""")
# Load names from link.
response = urlopen(url)
data_json = json.loads(response.read())
names = [feature['properties']['name'] for feature in data_json['features']]
# Create small example app.
app = Dash(__name__)
app.layout = html.Div([
    dl.Map([
        dl.TileLayer(url="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png"),
        dl.GeoJSON(
            url=url,
            zoomToBounds=True,
            id="state-layer",
            hideout=dict(selected=[]),
            options=dict(style=style_handle))
    ], style={'width': '100%', 'height': '600px'}, id="map"),
    dcc.Dropdown(
        id='search-bar',
        options=names,
        placeholder='Search State...',
        clearable=True,
        multi=True
    )
])
# Link drop down selection to GeoJSON.
app.clientside_callback("function(value){return {selected: value? value : []}}",
                        Output("state-layer", "hideout"),
                        Input('search-bar', "value"))

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

2 Likes

Thank @Emil.
Another question, can I make both effects, If I click the map it filters the dropdown, and If I filter the dropdown it will highlight the map?

Sure. Then you’ll jut add another callback that updates (toggles) the selection on click events,

import json
import dash_leaflet as dl
from urllib.request import urlopen
from dash import Dash, dcc, html, Output, Input, State
from dash_extensions.javascript import assign

url = "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json"
# Color selected state(s) red.
style_handle = assign("""function(feature, context){
    const {selected} = context.props.hideout;
    if(selected.includes(feature.properties.name)){
        return {fillColor: 'red'}
    }
}""")
# Load names from link.
response = urlopen(url)
data_json = json.loads(response.read())
names = [feature['properties']['name'] for feature in data_json['features']]
# Create small example app.
app = Dash(__name__)
app.layout = html.Div([
    dl.Map([
        dl.TileLayer(url="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png"),
        dl.GeoJSON(
            url=url,
            zoomToBounds=True,
            id="state-layer",
            hideout=dict(selected=[]),
            options=dict(style=style_handle))
    ], style={'width': '100%', 'height': '600px'}, id="map"),
    dcc.Dropdown(
        id='search-bar',
        options=names,
        placeholder='Search State...',
        clearable=True,
        multi=True,
    )
])
# Link drop down selection to GeoJSON.
app.clientside_callback("function(value){return {selected: value? value : []}}",
                        Output("state-layer", "hideout"),
                        Input('search-bar', "value"))
# Toggle select on click.
toggle_select = """function(_, feature, value){
const selected = value? value : [];
const name = feature.properties.name;
if(selected.includes(name)){selected.pop(name);}
else{selected.push(name);}
return selected;
}"""
app.clientside_callback(toggle_select,
                        Output("search-bar", "value"),
                        Input("state-layer", "n_clicks"),
                        State("state-layer", "click_feature"),
                        State("search-bar", "value"),
                        prevent_initial_call=True)

if __name__ == '__main__':
    app.run_server(debug=True)
2 Likes