Dynamically update multiple dropdowns without create an infinite callback loop

Hello,

First of all, I would like to thank the Dash community, it is a wonderful tool and you guys are awesome!!

I believe I hit a road block, I’ve spent days googling and experimenting, but I haven’t been successful so far. Basically I have a Pandas dataframe and I create multiple dropdowns to each column with dropdowns containing column’s unique values. For example, the dataframe may look like this:

col-A col-B col-C col-D
A1 B1 C1 D1
A1 B1 C2 D2
A2 B2 C2 D3
A2 B2 C3 D4

and the dropdowns have value of: d-A: {A1, A2}, d-B: {B1, B2}, d-C: {C1, C2, C3}, d-D: {D1, D2, D3, D4}

What I’m trying to achieve is a mimic of Spotfire filter panel, i.e., changing value of ANY of these dropdowns would dynamically update the rest of dropdowns by recalculating the unique values. For example,

1> if user picks A1 in d-A, the rest of 3 dropdowns becomes d-B: {B1}, d-C: {C1, C2}, d-D: {D1, D2}
2> if user pick C2 in d-C, the rest of 3 dropdowns becomes d-A: {A1, A2}, d-B: {B1, B2}, d-D: {D2, D3}

As you can see, the difficulty is that each dropdown is an output and an input simultaneously, and also we don’t know which dropdown users decide to click first. So that’ll create a infinite callback loop. I wonder if there’s a way to overcome this problem. Thank you in advance!

Jake

What I see working is having a dcc.Store component that gets updated whenever any of the dropdown’s value changes (first call back shown below). This is then the input for the 2nd callback that allows you to output the dropdown options as desired.

See https://dash.plot.ly/dash-core-components/store for more on dcc.Store

@app.callback(
  output = Output('dccStore_component', 'data'),
  inputs = [Input('dropdown_A', 'value'),
            Input('dropdown_B', 'value'),
            Input('dropdown_C', 'value'),
            Input('dropdown_D', 'value')])
def set_store_to_dropdown(dropA, dropB, dropC, dropD):
  # Use ctx = dash.callback_context to determine which input was selected
  # return desired dropdown information to dcc.Store component


@app.callback(
  inputs=[Input('dccStore_component', 'data')],
  output = [Output('dropdown_A', 'options'),
            Output('dropdown_B', 'options'),
            Output('dropdown_C', 'options'),
            Output('dropdown_D', 'options')])
def update_dropdown_based_on_change_to_dcc_store_component(data):
  # Process new options based on dcc.Store value

  # return dropdown options based on change to dcc.Store
  return new_A_options, new_B_options, new_C_otions, new_D_options

Depending on your use case, you may also need to pass in the value parameters to the second callback, as a value may no longer be valid (due to options changing). The code below is prob not the best implementation, but written to highlight how it could be done.

from dash.dash import no_update

...

@app.callback(
  inputs=[Input('dccStore_component', 'data')],
  output = [Output('dropdown_A', 'options'),
            Output('dropdown_B', 'options'),
            Output('dropdown_C', 'options'),
            Output('dropdown_D', 'options'),
            Output('dropdown_A', 'value'),
            Output('dropdown_B', 'value'),
            Output('dropdown_C', 'value'),
            Output('dropdown_D', 'value'],
  state = [State('dropdown_A', 'value'),
            State('dropdown_B', 'value'),
            State('dropdown_C', 'value'),
            State('dropdown_D', 'value']))
def update_dropdown_based_on_change_to_dcc_store_component(data, valA, valB, valC, valD):
  # Process new options based on dcc.Store value
  # You can also keep track of which dropdown was selected  within this dcc.Store
  dropdown_selected = data.selected #pseudo code just to highlight

  # return dropdown options based on change to dcc.Store
  # no_update is used to not change a components value
  if dropdownA:
    return no_update, new_B_options, new_C_otions, new_D_options, no_update, '', '', ''
  elif dropdownB:
    return new_A_options, no_update, new_C_otions, new_D_options, '', no_update, '', ''
  elif dropdownC:
    return new_A_options, new_B_options, no_update, new_D_options, '', '', no_update, ''
  elif dropdownD:
    return new_A_options, new_B_options, new_C_otions, no_update, '', '', '', no_update

Thanks flyingcujo for the quick reply.

I will give it a try and report it back. One question upfront though: from what you sketched in the second post, dropdown value change will trigger a dcc.Store component, then dcc.Store data change will trigger back dropdown value and options, isn’t that a infinite loop?

Jake

So I’ve tried the first post and it’s working, even without dcc.Store!! The trick is to use value as input, and options as output. If Dash detects new option list doesn’t contain an existing value item, it’ll remove that item from value list, that does the magic!!

I’ll take a look at dcc.Store document and see if that can make it even better.

# -*- coding: utf-8 -*-
# region impots

import dash
import dash_core_components as dcc
import dash_daq as daq
import dash_html_components as html
import pandas as pd
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate


data = [['A1', 'B1', 'C1', 'D1'], ['A1', 'B1', 'C2', 'D2'],
        ['A2', 'B2', 'C2', 'D3'], ['A2', 'B2', 'C3', 'D4']]
df = pd.DataFrame(data, columns=['A', 'B', 'C', 'D'])

optA = [{"label": _, "value": _} for _ in df["A"].unique()]
optB = [{"label": _, "value": _} for _ in df["B"].unique()]
optC = [{"label": _, "value": _} for _ in df["C"].unique()]
optD = [{"label": _, "value": _} for _ in df["D"].unique()]

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(
        id='A',
        options=optA,
        value=[_['value'] for _ in optA],
        multi=True,
    ),
    dcc.Dropdown(
        id='B',
        options=optB,
        value=[_['value'] for _ in optB],
        multi=True,
    ),
    dcc.Dropdown(
        id='C',
        options=optC,
        value=[_['value'] for _ in optC],
        multi=True,
    ),
    dcc.Dropdown(
        id='D',
        options=optD,
        value=[_['value'] for _ in optD],
        multi=True,
    ),
    dcc.Store(id='dccStore', storage_type='local'),
])


@app.callback(
    output=[Output('A', 'options'),
            Output('B', 'options'),
            Output('C', 'options'),
            Output('D', 'options')],
    inputs=[Input('A', 'value'),
            Input('B', 'value'),
            Input('C', 'value'),
            Input('D', 'value')])
def set_store_to_dropdown(dropA, dropB, dropC, dropD):
    ctx = dash.callback_context
    tmp = ctx.triggered[0]['prop_id'].split('.')[0]
    if tmp == "A":
        input = dropA
    elif tmp == "B":
        input = dropB
    elif tmp == "C":
        input = dropC
    elif tmp == "D":
        input = dropD
    df1 = df
    df1 = df[df[tmp].isin(input)]
    r, _ = df.shape
    if r == 0:
        return (optA, optB, optC, optD)
    else:
        oA = [{"label": _, "value": _} for _ in df1["A"].unique()]
        oB = [{"label": _, "value": _} for _ in df1["B"].unique()]
        oC = [{"label": _, "value": _} for _ in df1["C"].unique()]
        oD = [{"label": _, "value": _} for _ in df1["D"].unique()]
    return (oA, oB, oC, oD)


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

Hi flyingcujo,

I tried your second suggestion with dropdown values as additional outputs and as suspected, it generates a “circular dependencies” error. See the code and error screenshot below. Did I do anything wrong and would you expect it to work?

# -*- coding: utf-8 -*-
# region impots

import dash
import dash_core_components as dcc
import dash_daq as daq
import dash_html_components as html
import pandas as pd
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate


data = [['A1', 'B1', 'C1', 'D1'], ['A1', 'B1', 'C2', 'D2'],
        ['A2', 'B2', 'C2', 'D3'], ['A2', 'B2', 'C3', 'D4']]
df = pd.DataFrame(data, columns=['A', 'B', 'C', 'D'])

optA = [{"label": _, "value": _} for _ in df["A"].unique()]
optB = [{"label": _, "value": _} for _ in df["B"].unique()]
optC = [{"label": _, "value": _} for _ in df["C"].unique()]
optD = [{"label": _, "value": _} for _ in df["D"].unique()]

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(
        id='A',
        options=optA,
        value=[_['value'] for _ in optA],
        multi=True,
    ),
    dcc.Dropdown(
        id='B',
        options=optB,
        value=[_['value'] for _ in optB],
        multi=True,
    ),
    dcc.Dropdown(
        id='C',
        options=optC,
        value=[_['value'] for _ in optC],
        multi=True,
    ),
    dcc.Dropdown(
        id='D',
        options=optD,
        value=[_['value'] for _ in optD],
        multi=True,
    ),
    dcc.Store(id='dccStore', storage_type='local'),
])


@app.callback(
    output=Output('dccStore', 'data'),
    inputs=[Input('A', 'value'),
            Input('B', 'value'),
            Input('C', 'value'),
            Input('D', 'value')])
def set_store_to_dropdown(dropA, dropB, dropC, dropD):
    ctx = dash.callback_context
    tmp = ctx.triggered[0]['prop_id'].split('.')[0]
    if tmp == "A":
        input = dropA
    elif tmp == "B":
        input = dropB
    elif tmp == "C":
        input = dropC
    elif tmp == "D":
        input = dropD
    data = {}
    data['id'] = tmp
    data['value'] = input
    return data


@app.callback(
    inputs=[Input('dccStore', 'data')],
    output=[Output('A', 'options'),
            Output('B', 'options'),
            Output('C', 'options'),
            Output('D', 'options'),
            Output('A', 'value'),
            Output('B', 'value'),
            Output('C', 'value'),
            Output('D', 'value')],
    state=[State('A', 'value'),
           State('B', 'value'),
           State('C', 'value'),
           State('D', 'value')])
def update_based_on_change_to_dcc_store_component(data, valA, valB, valC, valD):
    return (optA, optB, optC, optD, [], [], [], [])

image

yeah…sorry about the 2nd post…i realized I might have mislead you when I was thinking about this solution on ride home. Glad the first posting was accurate!