Using a Multi-Dropdown for an App Where One Dropdown Populates Another

Hi Everyone,

I was wondering if someone knew how to incorporate the variable accumulating “Mult” attribute into the second dropdown in an app such as the one shown below (from an example in the user guide)?

It would ideally be able to result in a list with both American and Canadian cities. Simply adding:

Multi = True

to both dropdowns seems to break it. If anyone has a clever work-around that avoids populating a third dropdown I would be thrilled - thanks!


Thanks

1 Like

@Efidler12 - Hm, this should work. What have you tried so far?

Hello @Efidler12,

I am not quite sure of what you were asking…do you mean something like this?

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

USA = [
    "New York",
    "San Francisco",
    "Cincinnati",
    ]
CANADA = [
    "Montréal",
    "Toronto",
    "Ottawa",
    ]
country_dict = dict(USA = USA, CANADA = CANADA)

app = dash.Dash()
app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.layout = html.Div([
    dcc.Dropdown(
        id = "countries_dropdown",
        options = [
            { 'label': 'America', 'value': 'USA' },
            { 'label': 'Canada', 'value': 'CANADA' },
            ],
        value = [ "USA" ],
        multi = True,
        ),
    dcc.Dropdown(
        id = "cities_dropdown",
        ),
    ])
    
@app.callback(
    Output("cities_dropdown", "options"),
    [ Input("countries_dropdown", "value") ],
    )
def update_cities_options(countries):
    cities = []
    for country in countries:
        cities += [ dict(label = cities, value = cities) for cities in country_dict[country] ]
    return cities

@app.callback(
    Output("cities_dropdown", "value"),
    [ Input("cities_dropdown", "options") ],
    )
def update_cities_value(cities):
    if len(cities) != 0:
        return cities[0]
    else:
        return None


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

I have done this. My solution (rephrased for this context) was to return a list of all cities (as opposed to only those in the current country). You can (optionally) choose to display all cities but only have those belonging to the selected country enabled (other cities will be grayed out).

Something like this…

USA = [
    "New York",
    "San Francisco",
    "Cincinnati",
    ]
CANADA = [
    "Montréal",
    "Toronto",
    "Ottawa",
    ]

cities = USA + CANADA

city_to_country_dict = {}
for city in cities:
    if city in USA:
        city_to_country_dict[city] = 'USA'
    if city in CANADA:
        city_to_country_dict[city] = 'CANADA'         
    
@app.callback(
    dash.dependencies.Output('cities_dropdown', 'options'),
    [dash.dependencies.Input('countries_dropdown', 'value')])
def update_selectable_cities(selected_country):
    all_cities = []
    # add cities in country first so they are at the top
    # flag them "Y"
    for city in city_to_country_dict:
        if city_to_country_dict[city] == selected_country:
            all_cities.append([city, "Y"])
    # add cities NOT in country last so they are at the bottom
    # flag them "N"        
    for city in city_to_country_dict:
        if city_to_country_dict[city] != selected_country:
            all_cities.append([city, "N"])    
            
    return [{'label': i[0], 'value': i[0]} if i[1] == "Y"
             else {'label': i[0], 'value': i[0], 'disabled': 'True'}
             for i in all_cities]

Hi @chriddyp - really appreciate this tool! So what I tried is just adding the “Multi=True” to both dropdowns, hoping that the lower dropdown could “remember” what was selected when USA was the country in the first dropdown, even when the country is now Canada. This didn’t work…any thoughts? Thanks!

@Kefeng your solution allows the first dropdown to add options the second but clears the second when the first is changed (I had to add a “multi=True” to the second dropdown btw).

@oligosoma I’m afraid your solution is leaving everything grayed out for me but that is a good idea.

I should also have mentioned that my use case involves situations where the second variable will have the same name across multiple values in the first i.e. there is a Paris in Texas as well as France…I may have to do some re-labeling or something. Thanks everyone for the help so far!

Hello @Efidler12,

Still not very sure about what you want. From what I understand, I added a hidden div to “save” the cities dropdown value at each callback.

import dash, json
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html


USA = [
    "New York",
    "San Francisco",
    "Cincinnati",
    ]
CANADA = [
    "Montréal",
    "Toronto",
    "Ottawa",
    ]
country_dict = dict(USA = USA, CANADA = CANADA)

app = dash.Dash()
app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.layout = html.Div([
    dcc.Dropdown(
        id = "countries_dropdown",
        options = [
            { 'label': 'America', 'value': 'USA' },
            { 'label': 'Canada', 'value': 'CANADA' },
            ],
        value = [ "USA" ],
        multi = True,
        ),
    dcc.Dropdown(
        id = "cities_dropdown",
        multi = True,
        ),
    html.Div(id = "hidden_city", style = dict(display = "none"))
    ])
    
@app.callback(
    Output("cities_dropdown", "options"),
    [ Input("countries_dropdown", "value") ],
    )
def update_cities_options(countries):
    cities = []
    for country in countries:
        cities += [ dict(label = cities, value = cities) for cities in country_dict[country] ]
    return cities

@app.callback(
    Output("cities_dropdown", "value"),
    [ Input("cities_dropdown", "options") ],
    [ State("hidden_city", "children") ],
    )
def update_cities_value(cities, jcities):
    if jcities is None:
        return cities[0]
    else:
        return json.loads(jcities)
    
@app.callback(
    Output("hidden_city", "children"),
    [ Input("cities_dropdown", "value") ],
    )
def update_hidden_city(cities):
    return json.dumps(cities)


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

This code “remembers” what you selected for each country. For instance, if you select Ottawa and Montréal in Canada, change to USA and switch back to Canada, Ottawa and Montréal will still be selected.
If you want to have New York when only Canada is selected, I guess it might be a little bit tricky as Dash aims for UI consistency.

@Kefeng yes I want it to remember that I chose New York, say to update a plot with data from NY, but then I want to choose Canada, Montreal and add that data to the same plot. Except when I highlight Canada I need it to only allow me to add a city from Canada to the data being fed to the plot. Does that make more sense? Thanks

Here is the approach I followed: (editing common example with my approach)

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash(name)

all_options = {
‘America’: [‘New York City’, ‘San Francisco’, ‘Cincinnati’],
‘Canada’: [u’Montréal’, ‘Toronto’, ‘Ottawa’]
}
app.layout = html.Div([
dcc.Dropdown(
id=‘countries-dropdown’,
options=[{‘label’: k, ‘value’: k} for k in all_options.keys()],
value=‘America’, #default value to show
multi=True,
searchable=False
),

dcc.Dropdown(id='cities-dropdown', multi=True, searchable=False, placeholder="Select a city"),

html.Div(id='display-selected-values')

])

@app.callback(
dash.dependencies.Output(‘cities-dropdown’, ‘options’),
[dash.dependencies.Input(‘countries-dropdown’, ‘value’)])
def set_cities_options(selected_country):
if type(selected_country) == ‘str’:
return [{‘label’: i, ‘value’: i} for i in all_options[selected_country]]
else:
return [{‘label’: i, ‘value’: i} for country in selected_country for i in all_options[country]]

if name == ‘main’:
app.run_server(debug=True)

Workaround here is: When there is single input present in parent dropdown, the value is in string format. But for multiple values, it comes in list format.
This code also work perfectly and gets updated automatically even when you click on cross option to remove any selected option.

Note: I have used ‘placeholder’ attribute instead of defining default value for it as it made no sense in this case. But you can also update the value dynamically in similar way.