Dash Plotly - Chained Callbacks with Multiple Select, Disctinct Filtering and Dynamic Dataframe

I have been working through a scenario of chained callbacks and dynamic data frames, but not creating the results that I would like to achieve.

My objective is to allow for multiple, and clearable selections for each of the dropdowns, while each selection made filters the dataframe (df) into a dynamic dataframe (ddf) that is then used for graphics and insights.

If any selection is made in any order, all other options in the dropdowns are populated with the distinct values from the selection made and multiple selections can still be made.

Ex. If selection is made in option3 = Fall & Winter, then option1 would be limited to A & B, option2 would be limited to January, February, September, October, & December, and option3 would result in 15, 30, 80, 75, 75, & 25.

The issue that I see is that if I choose option3 = Fall, the dynamic dataframe is then limited to that option within the ddf and not the remaining 3 options of Winter, Summer and Spring as in the initial dataframe by the way that I’ve written this.

I feel that I may be going about this entirely wrong and making this more difficult of an item than required.

Any feedback or correction is greatly appreciated!



import pandas as pd
from dash import Dash, dcc, html, Input, Output, dash_table
import dash_bootstrap_components as dbc

d = [{'option1': 'A', 'option2': 'January', 'option3': 'Winter', 'option4': '15'},
     {'option1': 'A', 'option2': 'February', 'option3': 'Winter', 'option4': '30'},
     {'option1': 'A', 'option2': 'March', 'option3': 'Spring', 'option4': '35'},
     {'option1': 'A', 'option2': 'April', 'option3': 'Spring', 'option4': '40'},
     {'option1': 'A', 'option2': 'May', 'option3': 'Spring', 'option4': '50'},
     {'option1': 'A', 'option2': 'June', 'option3': 'Summer', 'option4': '70'},
     {'option1': 'B', 'option2': 'July', 'option3': 'Summer', 'option4': '90'},
     {'option1': 'B', 'option2': 'August', 'option3': 'Summer', 'option4': '100'},
     {'option1': 'B', 'option2': 'September', 'option3': 'Fall', 'option4': '80'},
     {'option1': 'B', 'option2': 'October', 'option3': 'Fall', 'option4': '75'},
     {'option1': 'B', 'option2': 'November', 'option3': 'Fall', 'option4': '75'},
     {'option1': 'B', 'option2': 'December', 'option3': 'Winter', 'option4': '25'}]

df = pd.DataFrame(d)


option1_list = sorted(df['option1'].unique().tolist())
option2_list = df['option2'].unique().tolist()
option3_list = sorted(df['option3'].unique().tolist())
option4_list = sorted(df['option4'].unique().tolist())

app = Dash(__name__)

app.layout = html.Div([
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 1"),
                                html.Div([
                                    dcc.Dropdown(id='option1_dropdown',
                                                 options=option1_list,
                                                 value='All',
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 2"),
                                html.Div([
                                    dcc.Dropdown(id='option2_dropdown',
                                                 options=option2_list,
                                                 value='All',
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 3"),
                                html.Div([
                                    dcc.Dropdown(id='option3_dropdown',
                                                 options=option3_list,
                                                 value='All',
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 4"),
                                html.Div([
                                    dcc.Dropdown(id='option4_dropdown',
                                                 options=option4_list,
                                                 value='All',
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        html.Div(id='dd-output-container')
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        dash_table.DataTable(
                            id='table_container',
                            data=df.to_dict('records')
                        )
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    )
])


@app.callback(
    Output('table_container', 'data'),
    Output('option1_dropdown', 'options'),
    Output('option2_dropdown', 'options'),
    Output('option3_dropdown', 'options'),
    Output('option4_dropdown', 'options'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])
def set_dropdown_options(value1, value2, value3, value4):
    #  If value lists are empty or equal to the default of 'All', use the initial df values
    if not value1 or value1 == 'All':
        value1 = option1_list
    if not value2 or value2 == 'All':
        value2 = option2_list
    if not value3 or value3 == 'All':
        value3 = option3_list
    if not value4 or value4 == 'All':
        value4 = option4_list

    ddf = df.query('option1 == @value1 and '
                   'option2 == @value2 and '
                   'option3 == @value3 and '
                   'option4 == @value4',
                   engine='python')

    if len(value1) < len(option1_list):
        # print(f'option1_list Line 151: {option1_list}')
        options1 = sorted(list(set(option1_list) - set(value1)))
        for i in options1:
            value1.append(i)
        value2 = ddf['option2'].unique().tolist()
        value3 = sorted(ddf['option3'].unique().tolist())
        value4 = sorted(ddf['option4'].unique().tolist())

    if len(value2) < len(option2_list):
        options2 = sorted(list(set(option2_list) - set(value2)))
        for i in options2:
            value2.append(i)
        value1 = sorted(ddf['option1'].unique().tolist())
        value3 = sorted(ddf['option3'].unique().tolist())
        value4 = sorted(ddf['option4'].unique().tolist())

    if len(value3) < len(option3_list):
        options3 = sorted(list(set(option3_list) - set(value3)))
        for i in options3:
            value3.append(i)
        value1 = sorted(ddf['option1'].unique().tolist())
        value2 = ddf['option2'].unique().tolist()
        value4 = sorted(ddf['option4'].unique().tolist())

    if len(value4) < len(option4_list):
        options4 = sorted(list(set(option4_list) - set(value4)))
        for i in options4:
            value4.append(i)
        value1 = sorted(ddf['option1'].unique().tolist())
        value2 = ddf['option2'].unique().tolist()
        value3 = sorted(ddf['option3'].unique().tolist())

    return ddf.to_dict('records'), value1, value2, value3, value4

# ====== Using this as a way to view the selections
@app.callback(
    Output('dd-output-container', 'children'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])
def selection(value1, value2, value3, value4):
    #  If value lists are empty or equal to the default of 'All', use the initial df values
    if not value1 or value1 == 'All':
        value1 = option1_list
    if not value2 or value2 == 'All':
        value2 = option2_list
    if not value3 or value3 == 'All':
        value3 = option3_list
    if not value4 or value4 == 'All':
        value4 = option4_list
    return f'Selected Option 1: {value1} Option 2: {value2} Option 3: {value3} Option 4: {value4} '


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

1 Like

I think we need to find each option based on value of the remaining dropdowns.
I did something as below, please check if it was your expectation.

import pandas as pd
from dash import Dash, dcc, html, Input, Output, dash_table
import dash_bootstrap_components as dbc

d = [{'option1': 'A', 'option2': 'January', 'option3': 'Winter', 'option4': '15'},
     {'option1': 'A', 'option2': 'February', 'option3': 'Winter', 'option4': '30'},
     {'option1': 'A', 'option2': 'March', 'option3': 'Spring', 'option4': '35'},
     {'option1': 'A', 'option2': 'April', 'option3': 'Spring', 'option4': '40'},
     {'option1': 'A', 'option2': 'May', 'option3': 'Spring', 'option4': '50'},
     {'option1': 'A', 'option2': 'June', 'option3': 'Summer', 'option4': '70'},
     {'option1': 'B', 'option2': 'July', 'option3': 'Summer', 'option4': '90'},
     {'option1': 'B', 'option2': 'August', 'option3': 'Summer', 'option4': '100'},
     {'option1': 'B', 'option2': 'September', 'option3': 'Fall', 'option4': '80'},
     {'option1': 'B', 'option2': 'October', 'option3': 'Fall', 'option4': '75'},
     {'option1': 'B', 'option2': 'November', 'option3': 'Fall', 'option4': '75'},
     {'option1': 'B', 'option2': 'December', 'option3': 'Winter', 'option4': '25'}]

df = pd.DataFrame(d)


option1_list = sorted(df['option1'].unique().tolist())
option2_list = df['option2'].unique().tolist()
option3_list = sorted(df['option3'].unique().tolist())
option4_list = sorted(df['option4'].unique().tolist())

app = Dash(__name__)

app.layout = html.Div([
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 1"),
                                html.Div([
                                    dcc.Dropdown(id='option1_dropdown',
                                                 options=option1_list,
                                                 value=[],
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 2"),
                                html.Div([
                                    dcc.Dropdown(id='option2_dropdown',
                                                 options=option2_list,
                                                 value=[],
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 3"),
                                html.Div([
                                    dcc.Dropdown(id='option3_dropdown',
                                                 options=option3_list,
                                                 value=[],
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
                dbc.Col([
                    html.Div([
                        dbc.Card(
                            dbc.CardBody([
                                html.P("Option 4"),
                                html.Div([
                                    dcc.Dropdown(id='option4_dropdown',
                                                 options=option4_list,
                                                 value=[],
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                    style={'width': '100%', 'display': 'inline-block'})
                            ])
                        ),
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        html.Div(id='dd-output-container')
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    html.Div([
                        dash_table.DataTable(
                            id='table_container',
                            data=df.to_dict('records')
                        )
                    ])
                ]),
            ], align='center'),
        ]), color='dark'
    )
])


@app.callback(
    Output('option1_dropdown', 'options'),
    [Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])

def set_dropdown_options(value2, value3, value4):
    dff = df.copy()     
    if value2:
        dff = dff[dff['option2'].isin(value2)]
    if value3:
        dff = dff[dff['option3'].isin(value3)]
    if value4:
        dff = dff[dff['option4'].isin(value4)]          

    return [{'label':x,'value':x} for x in dff['option1'].unique()]



@app.callback(
    Output('option2_dropdown', 'options'),
    [Input('option1_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])

def set_dropdown_options(value1, value3, value4):
    dff = df.copy()    
    if value1:
        dff = dff[dff['option1'].isin(value1)]
    if value3:
        dff = dff[dff['option3'].isin(value3)]
    if value4:
        dff = dff[dff['option4'].isin(value4)]          
    return [{'label':x,'value':x} for x in dff['option2'].unique()]


@app.callback(
    Output('option3_dropdown', 'options'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option4_dropdown', 'value')])

def set_dropdown_options(value1, value2, value4):
    dff = df.copy()
    if value1:
        dff = dff[dff['option1'].isin(value1)]
    if value2:
        dff = dff[dff['option2'].isin(value2)]
    if value4:
        dff = dff[dff['option4'].isin(value4)]          
    return [{'label':x,'value':x} for x in dff['option3'].unique()]


@app.callback(
    Output('option4_dropdown', 'options'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value')])

def set_dropdown_options(value1, value2, value3):
    dff = df.copy()
    if value1:
        dff = dff[dff['option1'].isin(value1)]
    if value2:
        dff = dff[df['option2'].isin(value2)]
    if value3:
        dff = dff[dff['option3'].isin(value3)]          
    return [{'label':x,'value':x} for x in dff['option4'].unique()]

@app.callback(
    Output('table_container', 'data'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])
def set_dropdown_options(value1, value2, value3, value4):
    if not value1 or value1 == 'All':
        value1 = option1_list
    if not value2 or value2 == 'All':
        value2 = option2_list
    if not value3 or value3 == 'All':
        value3 = option3_list
    if not value4 or value4 == 'All':
        value4 = option4_list
        
    ddf = df.query('option1 == @value1 and '
                   'option2 == @value2 and '
                   'option3 == @value3 and '
                   'option4 == @value4',
                   engine='python')
    
    return ddf.to_dict('records')

# ====== Using this as a way to view the selections
@app.callback(
    Output('dd-output-container', 'children'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value')])
def selection(value1, value2, value3, value4):
    #  If value lists are empty or equal to the default of 'All', use the initial df values
    if not value1 or value1 == 'All':
        value1 = option1_list
    if not value2 or value2 == 'All':
        value2 = option2_list
    if not value3 or value3 == 'All':
        value3 = option3_list
    if not value4 or value4 == 'All':
        value4 = option4_list
        
    ddf = df.query('option1 == @value1 and '
                   'option2 == @value2 and '
                   'option3 == @value3 and '
                   'option4 == @value4',
                   engine='python')    
    value1 = ddf['option1'].unique()
    value2 = ddf['option2'].unique()
    value3 = ddf['option3'].unique()
    value4 = ddf['option4'].unique()
    return f'Selected Option 1: {value1} Option 2: {value2} Option 3: {value3} Option 4: {value4} '

if __name__ == '__main__':
    app.run_server(debug=False, use_reloader=False)

Recording 2023-04-06 105704

2 Likes

What an amazing community! Thank you so much @hoatran ! This now makes absolute sense to me and is greatly appreciated.

1 Like