Search code examples
javascriptpythoncallbackplotly-dashdashboard

Getting error when using clientside_callback via Java Script in Dash - Python


I've recently asked a question about how to use clientside_callback (see this) and am practicing it on my own dashboard application. In my dashboard, I have a map, a drop down menu, and a button. The user selects states on the map with a click, and can also select them from the drop down menu. However, there is ALL option in the drop down menu as well. As for the button, it clears the user's selection.

My application works with the regular Dash callbacks, but my goal is to use clientside_callback to speed up the process. However, I receive multiple errors with my code due to the Java Script part about which I have no experience.

That's why I'd appreciate if someone could assist me.

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 import dash_table


#https://gist.github.com/incubated-geek-cc/5da3adbb2a1602abd8cf18d91016d451?short_path=2de7e44
us_states_gdf = gpd.read_file("us_states.geojson")
us_states_geojson = json.loads(us_states_gdf.to_json())

options = [{'label': 'Select all', 'value': 'ALL'}, {'label': 'AK', 'value': 'AK'}, {'label': 'AL', 'value': 'AL'},
           {'label': 'AR', 'value': 'AR'}, {'label': 'AZ', 'value': 'AZ'}, {'label': 'CA', 'value': 'CA'},
           {'label': 'CO', 'value': 'CO'}, {'label': 'CT', 'value': 'CT'}, {'label': 'DE', 'value': 'DE'},
           {'label': 'FL', 'value': 'FL'}, {'label': 'GA', 'value': 'GA'}, {'label': 'HI', 'value': 'HI'},
           {'label': 'IA', 'value': 'IA'}, {'label': 'ID', 'value': 'ID'}, {'label': 'IL', 'value': 'IL'},
           {'label': 'IN', 'value': 'IN'}, {'label': 'KS', 'value': 'KS'}, {'label': 'KY', 'value': 'KY'},
           {'label': 'LA', 'value': 'LA'}, {'label': 'MA', 'value': 'MA'}, {'label': 'MD', 'value': 'MD'},
           {'label': 'ME', 'value': 'ME'}, {'label': 'MI', 'value': 'MI'}, {'label': 'MN', 'value': 'MN'},
           {'label': 'MO', 'value': 'MO'}, {'label': 'MS', 'value': 'MS'}, {'label': 'MT', 'value': 'MT'},
           {'label': 'NC', 'value': 'NC'}, {'label': 'ND', 'value': 'ND'}, {'label': 'NE', 'value': 'NE'},
           {'label': 'NH', 'value': 'NH'}, {'label': 'NJ', 'value': 'NJ'}, {'label': 'NM', 'value': 'NM'},
           {'label': 'NV', 'value': 'NV'}, {'label': 'NY', 'value': 'NY'}, {'label': 'OH', 'value': 'OH'},
           {'label': 'OK', 'value': 'OK'}, {'label': 'OR', 'value': 'OR'}, {'label': 'PA', 'value': 'PA'},
           {'label': 'RI', 'value': 'RI'}, {'label': 'SC', 'value': 'SC'}, {'label': 'SD', 'value': 'SD'},
           {'label': 'TN', 'value': 'TN'}, {'label': 'TX', 'value': 'TX'}, {'label': 'UT', 'value': 'UT'},
           {'label': 'VA', 'value': 'VA'}, {'label': 'VT', 'value': 'VT'}, {'label': 'WA', 'value': 'WA'},
           {'label': 'WI', 'value': 'WI'}, {'label': 'WV', 'value': 'WV'}, {'label': 'WY', 'value': 'WY'}]

state_abbreviations = {'Alabama': 'AL', 'Alaska': 'AK', 'Arizona': 'AZ', 'Arkansas': 'AR', 'California': 'CA', 'Colorado': 'CO',
          'Connecticut': 'CT', 'Delaware': 'DE', 'Florida': 'FL', 'Georgia': 'GA', 'Hawaii': 'HI', 'Idaho': 'ID',
          'Illinois': 'IL', 'Indiana': 'IN', 'Iowa': 'IA', 'Kansas': 'KS', 'Kentucky': 'KY', 'Louisiana': 'LA',
          'Maine': 'ME', 'Maryland': 'MD', 'Massachusetts': 'MA', 'Michigan': 'MI', 'Minnesota': 'MN',
          'Mississippi': 'MS', 'Missouri': 'MO', 'Montana': 'MT', 'Nebraska': 'NE', 'Nevada': 'NV',
          'New Hampshire': 'NH', 'New Jersey': 'NJ', 'New Mexico': 'NM', 'New York': 'NY', 'North Carolina': 'NC',
          'North Dakota': 'ND', 'Ohio': 'OH', 'Oklahoma': 'OK', 'Oregon': 'OR', 'Pennsylvania': 'PA',
          'Rhode Island': 'RI', 'South Carolina': 'SC', 'South Dakota': 'SD', 'Tennessee': 'TN', 'Texas': 'TX',
          'Utah': 'UT', 'Vermont': 'VT', 'Virginia': 'VA', 'Washington': 'WA', 'West Virginia': 'WV', 'Wisconsin':
              'WI', 'Wyoming': 'WY'}
states =  list(state_abbreviations.values())
app = Dash(__name__)
app.layout = html.Div([
#Store lists to use in the callback
    dcc.Store(id='options-store', data=json.dumps(options)),
    dcc.Store(id='states-store', data=json.dumps(states)),
    dcc.Store(id='states-abbrevations', data=json.dumps(state_abbreviations)),
#US Map here
    dl.Map([
        dl.TileLayer(url="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png"),
        dl.GeoJSON(data=us_states_geojson, id="state-layer")],
        style={'width': '100%', 'height': '250px'},
        id="map",
        center=[39.8283, -98.5795],
    ),
#Drop down menu here
    html.Div(className='row', children=[
        dcc.Dropdown(
            id='state-dropdown',
            options=[{'label': 'Select all', 'value': 'ALL'}] +
                    [{'label': state, 'value': state} for state in states],
            value=[],
            multi=True,
            placeholder='States'
        )]),
    html.Div(className='one columns', children=[
        html.Button(
            'Clear',
            id='clear-button',
            n_clicks=0,
            className='my-button'
        ),
    ]),

])


@app.callback(
    Output('state-dropdown', 'value', allow_duplicate=True),
    [Input('clear-button', 'n_clicks')],
    prevent_initial_call=True
)
def clear_tab(user_click):
    if user_click:
        return []
    else:
        raise dash.exceptions.PreventUpdate

app.clientside_callback(
    """
    function(click_feature, selected_states, defaults_options, states, states_abbreviations) {

        let options = defaults_options
        let select_all_selected = selected_states.includes('ALL');
        let list_states;


        if (select_all_selected) {
            options = [{'label': 'Select All', 'value': 'ALL'}];
            selected_states = states;
            list_states = 'ALL';
        } else {
            list_states = selected_states;

            if (click_feature && dash.callback_context.triggered[0]['prop_id'].split('.')[0] == 'state-layer') {
                let state_name = state_abbreviations[click_feature["properties"]["NAME"]];
                if (!selected_states.includes(state_name)) {
                    selected_states.push(state_name);
                    list_states = selected_states;
                }
            }
        }

        return [options, list_states];
    }
     """,
    Output('state-dropdown', 'options'),
    Output('state-dropdown', 'value'),
    Input('state-layer', 'click_feature'),
    Input('state-dropdown', 'value'),
    State('options-store', 'data'),
    State('states-store', 'data'),
    State('states-abbrevations', 'data'),
    prevent_initial_call=True
)
if __name__ == '__main__':
    app.run_server(debug=True)


Solution

  • You don't need to serialize the store data into a JSON string (or by doing so you would have to use JSON.parse() clientside to unserialize them back, but Dash already does it internally) so the first thing is to fix that in the app layout in order to receive proper JS objects in your clientside callback :

    #Store lists to use in the callback
    dcc.Store(id='options-store', data=options),
    dcc.Store(id='states-store', data=states),
    dcc.Store(id='states-abbrevations', data=state_abbreviations),
    # ...
    

    The second thing is to use dash_clientside instead of dash in the clientside callback so you can get the callback context etc., and also fix a typo for states_abbreviations :

    function(click_feature, selected_states, defaults_options, states, states_abbreviations) {
        let options = defaults_options;
        let select_all_selected = selected_states.includes('ALL');
        let list_states;
    
        if (select_all_selected) {
            options = [{'label': 'Select All', 'value': 'ALL'}];
            selected_states = states;
            list_states = 'ALL';
        } else {
            list_states = selected_states;
    
            if (click_feature && dash_clientside.callback_context.triggered[0]['prop_id'].split('.')[0] == 'state-layer') {
                let state_name = states_abbreviations[click_feature["properties"]["NAME"]];
                if (!selected_states.includes(state_name)) {
                    selected_states.push(state_name);
                    list_states = selected_states;
                }
            }
        }
    
        return [options, list_states];
    }