Dynamically updating dropdown options with 2 Inputs in callback Dash

Question:

I am stuck in a part where I want to update the Batch dropdown in 2 ways. If there is nothing selected in Material dropdown, the batch dropdown should show all the Batches, but as soon as Material is selected, it should change to only show Batches for that Material type.

This is what the relevant code looks like right now:

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output 
import plotly.express as px
import pandas as pd
import re

app = dash.Dash(__name__)

# Convert PARQUET to CSV


df = pd.read_csv("release_cln.csv")

# Store all unique values from Basis column

df_dis = df[df['Analysis_Name_CLN'].str.contains("Dissolution").fillna(False)]

df_dis['Batch'] = df_dis['Batch'].fillna(9999)
df_dis['Basis'] = df_dis['Batch'].fillna(9999)
df_dis['Material'] = df['Material'].fillna(9999)

batch = df_dis['Batch'].unique()
material = df_dis['Material'].unique()
basis = df_dis['Basis'].unique()

#[{'label': i, 'value': i} for i in material]

app.layout = html.Div([
    html.H1(children='Comparing Batches'),
    html.H2('Dissolution vs Timepoint'),

    # Batch 1 Dropdowns
    html.Div([
        html.Div(
            html.H3(children='''DP Batch''')
        ),
        html.Div([
            html.Div(children='''Batch: '''),
            dcc.Dropdown(
                    id='batch_dd',
                    multi=False,
                    clearable=True,
                    disabled=False
                ),
            ],style={'width': '20%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Material: '''),
            dcc.Dropdown(
                    id='material_dd',
                    multi=False,
                    clearable=True,
                    disabled=False
                ),
            ],style = {'width': '15%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Basis: '''),
            dcc.Dropdown(
                    id='basis_dd',
                    multi=False,
                    clearable=True,
                    disabled=False
                ),
            ],style = {'width': '15%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Variant: '''),
            dcc.Dropdown(
                    id='variant_dd',
                    multi=False,
                    clearable=True,
                    disabled=False
                ),
            ],style = {'width': '10%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Analysis: '''),
            dcc.Dropdown(
                    id='analysis_name_dd',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[]
                ),
            ],style = {'width': '40%','display': 'inline-block'}
        )
        ],style = {'width': '90%', 'display': 'inline-block'}
    ),

    html.Br(),

    # Batch 2 Dropdowns
    html.Div([
        html.Div(
            html.H3(children='''DS Batch''')
        ),
        html.Div([
            html.Div(children='''Batch: '''),
            dcc.Dropdown(
                    id='batch_dd_1',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[{'label': i, 'value': i} for i in batch]
                ),
            ],style={'width': '20%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Material: '''),
            dcc.Dropdown(
                    id='material_dd_1',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[],
                ),
            ],style = {'width': '15%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Basis: '''),
            dcc.Dropdown(
                    id='basis_dd_1',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[]
                ),
            ],style = {'width': '15%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Variant: '''),
            dcc.Dropdown(
                    id='variant_dd_1',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[]
                ),
            ],style = {'width': '10%','display': 'inline-block'}
        ),
        html.Div([
            html.Div(children='''Analysis: '''),
            dcc.Dropdown(
                    id='analysis_name_dd_1',
                    multi=False,
                    clearable=True,
                    disabled=False,
                    options=[]
                ),
            ],style = {'width': '40%','display': 'inline-block'}
        )
        ],style = {'width': '90%', 'display': 'inline-block'}
    ),

    html.Br(),
    dcc.Graph(id='dissol_tp')
])

## Batch 1 Dropdowns
#Call back to update Batch options if user searches for Batches
@app.callback(
    Output('batch_dd','options'),
    [Input('batch_dd','search_value'),
    Input('material_dd','value')]
)
def batch_options(search_value, material_dd):
    if material_dd is None:
        return [{'label': i, 'value': i} for i in batch]
    else:
        batch_df = df[df['Material']==material_dd]
        return [{'label':i,'value':i} for i in batch_df['Batch'].fillna('None').unique()]


@app.callback(
    Output('material_dd','options'),
    Input('material_dd','search_value'))
def material_options(search_value):
    return [{'label': i, 'value': i} for i in material]

# Callback to select default value in basis list
@app.callback(
    Output('basis_dd','options'),
    Input('basis_dd','search_value'))
def variant_default(batch_dd):
    return [k['value'] for k in batch_dd]

# Chained callback to select variants with the selected batch
@app.callback(
    Output('variant_dd','options'),
    Input('batch_dd','value'))
def get_variant(batch_dd):
    batch_df = df[df['Batch']==batch_dd]
    return [{'label':i,'value':i} for i in batch_df['Variant'].fillna('None').unique()]


What’s happening currently is batch dropdown shows all the batch values even when a material value is selected and doesn’t change dynamically

Asked By: Raghav Chamadiya

||

Answers:

Dynamic Dropdown Live Updating

EDIT (Feb. 2023): Simplified code of original answer, also now demonstrating use of the optional Dash dcc.Dropdown multi=True parameter. *

Two UI dropdown elements are presented. The first is hard-coded and is a list of options of possible ‘materials’ corresponding to unique (randomly sampled) subsets of ‘batches’ (here simply represented as a total batch set with #’s 1-100). Each material is randomly sampled without replacement by 50 draws (i.e., k=50) from the total set (i.e., here, total_batches).

* User-Selected Multiple Options (multi=True)

If a user selects multiple materials options from the first dropdown, only those batches found in all selected subsets will be immediately dynamically updated in the second "batches" dropdown (calculated using set.intersection() on a list of set-cast lists [i.e., the materials-selected batch subsets]).

import random

import dash
import pandas as pd

from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output


app = dash.Dash(__name__)

total_batches = [n + 1 for n in range(100)]

materials = sorted(["wood", "metal", "fire", "water", "ice", "rock"])

df = pd.DataFrame(
    {"Batch": random.sample(total_batches, k=50), "Material": m}
    for m in materials
).set_index("Material")

batches = df.to_dict()["Batch"]

app.layout = html.Div(
    [
        html.H1(children="Comparing Batches"),
        html.H2("Dissolution vs Timepoint"),
        # Batch 1 Dropdowns
        html.Div(
            [
                html.Div(html.H3(children="""DP Batch""")),
                html.Div(
                    [
                        html.Div(children="""Material: """),
                        dcc.Dropdown(
                            id="material_dd",
                            options=[
                                {"label": m, "value": m} for m in materials
                            ],
                            multi=True, # Or set to False
                            clearable=True,
                            disabled=False,
                        ),
                    ],
                    style={"width": "15%", "display": "inline-block"},
                ),
                html.Div(
                    [
                        html.Div(children="""Batch: """),
                        dcc.Dropdown(
                            id="batch_dd",
                            multi=False,
                            clearable=True,
                            disabled=False,
                        ),
                    ],
                    style={"width": "20%", "display": "inline-block"},
                ),
            ]
        ),
    ]
)

## Dynamic Batches Dropdown Updates
# Callback to update Batch options dynamically upon user selection
# actions at the first dropdown ("Materials").
@app.callback(
    Output("batch_dd", "options"), [Input("material_dd", "value")],
)
def batch_options(material_dd):
    """Dynamically live-update the second dropdown based upon user interaction
    with the first.
    
    Args:
        material_dd (list [or str]): An array (if `multi=True` set for 
        `material_dd` Dropdown component, else str) containing the user-selected
        material from the first dropdown shown on user interface.
    
    Returns:
        list: An array containing a list of dict's of key-value pairs where the 
        keys are specifically "label" and "value", which correspond to the 
        dynamically live-updating options to be displayed in the second drop-
        down.
    """
    if material_dd is None or len(material_dd) < 1:
        return [{"label": i, "value": i} for i in sorted(total_batches)]
    elif type(material_dd) == list and len(material_dd) > 1:
        print(f"User has selected multiple materials options: {material_dd}")
        mult_batch_sets = [set(batches[m]) for m in material_dd]
        batches_intersect = list(set.intersection(*mult_batch_sets))
        print(batches_intersect)
        return [{"label": i, "value": i} for i in sorted(batches_intersect)]
    elif type(material_dd) == list:
        """NOTE: Because the 'multi' parameter for the `material_dd` dropdown
        is set `== True`, even if only one material is currently selected
        the value is returned as a single item list. Hence, the need
        for zero-indexing `material_dd` to prevent a "TypeError: unhashable type:
        'list'" when indexing the `batches` dictionary."""
        m = material_dd[0]
        print(f"User has selected single material value: {m}")
        # print(sorted(batches[material_dd[0]]))
        return [{"label": i, "value": i} for i in sorted(batches[m])]
    else: # ∴ `multi=False` must be set
        return [{"label": i, "value": i} for i in sorted(batches[material_dd])]


# Prints to stdout
# For debugging/demo purposes merely
# (In professional setup, Python stdlib `logging` should be used.)
print("n---New Batches random sampling simulation started---")
print(f"{'_'*40}")
print(f"All Batch #'s:n{sorted(total_batches)}")
print(f"Possible materials:n{materials}")
print(f"Batches subset by material type:n{batches}")
print("n...Awaiting user input...n")


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

Live demo of use of app created verbatim from above code.

Notice how with each addition of another user-selected option, the dynamically-synced second dropdown changes to show updated corresponding "batches" (in this case, each option [‘material’] has value = 50 unique randomly sampled integers from range 1-100) subsets from the total possible set (total_batches). And as expected, there is considerable, though diminishing, intersection of the randomly sampled subsets with the addition of each further option (‘material’).

(Ultimately, in this particular simulated instance, it just so happens 3 batch ID #’s were randomly drawn in every possible ‘materials’ option.)

Stderr reporting example (from live sample simulation recorded and shown above)

Dash is running on http://127.0.0.1:8050/

 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on

---New Batches random sampling simulation started---
________________________________________
All Batch #'s:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
Possible materials:
['fire', 'ice', 'metal', 'rock', 'water', 'wood']
Batches subset by material type:
{'fire': [97, 25, 37, 34, 90, 57, 8, 23, 28, 16, 38, 88, 13, 24, 36, 56, 50, 41, 40, 43, 81, 87, 71, 91, 62, 92, 69, 83, 1, 66, 19, 30, 58, 65, 68, 45, 59, 55, 29, 84, 7, 6, 77, 67, 3, 74, 18, 33, 72, 98],
 'ice': [28, 33, 77, 26, 4, 14, 13, 20, 72, 39, 56, 35, 50, 31, 24, 90, 17, 73, 63, 99, 29, 44, 98, 92, 47, 67, 79, 82, 41, 93, 87, 15, 71, 52, 2, 22, 94, 18, 34, 74, 1, 27, 38, 57, 84, 88, 68, 11, 49, 8],
 'metal': [19, 14, 90, 33, 34, 16, 63, 10, 53, 92, 46, 82, 78, 81, 64, 69, 31, 45, 73, 39, 68, 5, 38, 100, 24, 7, 61, 50, 83, 54, 62, 84, 74, 23, 32, 40, 1, 3, 51, 25, 79, 29, 91, 52, 88, 77, 57, 35, 58, 96],
 'rock': [9, 42, 13, 59, 44, 27, 72, 97, 70, 80, 65, 63, 53, 40, 85, 78, 90, 58, 60, 69, 98, 89, 46, 36, 88, 15, 37, 79, 95, 93, 62, 82, 7, 41, 94, 51, 20, 16, 4, 49, 99, 54, 76, 8, 48, 39, 19, 100, 67, 25],
 'water': [16, 10, 91, 71, 54, 41, 94, 26, 65, 87, 30, 39, 20, 60, 72, 18, 74, 5, 1, 76, 66, 77, 13, 6, 92, 36, 80, 24, 95, 25, 45, 96, 59, 78, 14, 2, 67, 82, 99, 83, 69, 8, 50, 29, 27, 93, 46, 70, 22, 49],
 'wood': [4, 69, 92, 83, 30, 61, 7, 35, 12, 93, 65, 8, 85, 72, 100, 68, 41, 88, 46, 60, 34, 37, 78, 33, 50, 77, 95, 96, 45, 39, 40, 48, 99, 3, 36, 18, 54, 5, 91, 80, 87, 31, 51, 23, 98, 94, 13, 20, 56, 59]}

...Awaiting user input...

User has selected single material value: fire
User has selected multiple materials options: ['fire', 'ice']
[18, 19, 20, 25, 27, 28, 29, 33, 35, 48, 53, 59, 63, 64, 66, 67, 70, 77, 80, 91, 92, 98]
User has selected multiple materials options: ['fire', 'ice', 'metal']
[98, 67, 77, 92, 48, 18, 19, 25, 59, 28, 29, 63]
User has selected multiple materials options: ['fire', 'ice', 'metal', 'rock']
[98, 67, 77, 18, 19, 59, 63]
User has selected multiple materials options: ['fire', 'ice', 'metal', 'rock', 'water']
[59, 77, 63]
User has selected multiple materials options: ['fire', 'ice', 'metal', 'rock', 'water', 'wood']
[59, 77, 63]


ORIGINAL ANSWER (no use of multi=True parameter)

NOTE: Edited answer code above is recommended (due to simpler, more proper Dash code) even if user ability to select multiple options from the first dropdown is not desired (in which case, simply make the change: multi=False to the dcc.Dropdown parameter in the defined app layout).

Here’s a demonstration, based off of your code, just showing how to get the batches dropdown to dynamically depend on whatever is selected in the material dropdown.

import random
import dash
import numpy as np
import pandas as pd

from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output


app = dash.Dash(__name__)

# Convert PARQUET to CSV
# df = pd.read_csv("release_cln.csv")

n = 50
df_dis = pd.DataFrame(
    {
        "Batch": np.random.randint(1, 2000, n),
        "Material": random.choices(
            ["wood", "metal", "fire", "water", "ice", "rock"], k=n
        ),
    }
)

batches = {
    m: list(df_dis[df_dis.Material == m].Batch.values)
    for m in df_dis.Material
}


batch = df_dis["Batch"].unique()
material = df_dis["Material"].unique()


app.layout = html.Div(
    [
        html.H1(children="Comparing Batches"),
        html.H2("Dissolution vs Timepoint"),
        # Batch 1 Dropdowns
        html.Div(
            [
                html.Div(html.H3(children="""DP Batch""")),
                html.Div(
                    [
                        html.Div(children="""Material: """),
                        dcc.Dropdown(
                            id="material_dd",
                            multi=False,
                            clearable=True,
                            disabled=False,
                        ),
                    ],
                    style={"width": "15%", "display": "inline-block"},
                ),
                html.Div(
                    [
                        html.Div(children="""Batch: """),
                        dcc.Dropdown(
                            id="batch_dd",
                            multi=False,
                            clearable=True,
                            disabled=False,
                        ),
                    ],
                    style={"width": "20%", "display": "inline-block"},
                ),
            ]
        ),
    ]
)

## Batch 1 Dropdowns
# Call back to update Batch options if user searches for Batches
@app.callback(
    Output("batch_dd", "options"),
    [Input("batch_dd", "search_value"), Input("material_dd", "value")],
)
def batch_options(search_value, material_dd):
    if material_dd is None:
        return [{"label": i, "value": i} for i in batch]
    else:
        return [{"label": i, "value": i} for i in batches[material_dd]]


@app.callback(
    Output("material_dd", "options"), Input("material_dd", "search_value")
)
def material_options(search_value):
    return [{"label": i, "value": i} for i in material]


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

demo

Here’s what I did:

  • Create a dictionary, where each unique material from the dataframe form the keys, and each one has a corresponding list (assuming all material possible values have >1 row in df), specifically of the Batches which have the selected material type.
  • Likewise you would proceed for further nesting of dropdowns
Answered By: John Collins
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.