pythonpandascallbackplotlychained

Partial chained callbacks using plotly dash


I'm trying to filter data using multiple dropdown bars within a plotly dashboard. There are 5 dropdown options in total. I want the first 3 to operate indepently, while the last two should be chained, both ways, to the first 3.

Specifically, the features that I'm aiming to implement are:

  1. A default of all values should always be the initial starting point

  2. The first 3 options (Year, Season and Month) should act independently. As in, any combination of these 3 can be added to the output. If one item is selected, the output should be updated with those values. However, if an item is selected from another dropdown, those values should be added to the output. Example below in i).

  3. Option 4-5 (temp and prec) should be chained, both ways, to the first three dropdown options (Year, Season and Month). This should be reversible or both ways too. If one of the first 3 dropdown options is selected, the table output should be updated with those values and the dropdown lists should be reduced to only allow the user to pick from those values. Example below in ii).

To provide concrete examples;

i) 2012 is selected from Year in the first dropdown option. The table output displays the relevant values. The user should be able to select any subsequent values in the Year dropdown list (functional). However, if the user wants to also see Spr values from the second dropdown option, that data should be added to the output.

ii) For the 4-5 dropdown options which should be chained to first 3, if Hot and Mild are selected in temp and Wet is selected in prec, then the dropdown lists in the first three options should be reduced to: Year = 2013, 2015; Season = Spr, Fall; Month = Apr, Jun, Oct, Dec.

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

Year = cycle(['2012','2013','2014','2015'])
Season = cycle(['Win','Spr','Sum','Fall'])
Month = cycle(['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])
temp_group = cycle(['Hot','Cold','Mild'])
prec_group = cycle(['Dry','Wet'])

df = pd.DataFrame(index = range(20))

df['option1'] = [next(Year) for count in range(df.shape[0])]
df['option2'] = [next(Season) for count in range(df.shape[0])]
df['option3'] = [next(Month) for count in range(df.shape[0])]
df['option4'] = [next(temp_group) for count in range(df.shape[0])]
df['option5'] = [next(prec_group) for count in range(df.shape[0])]

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

app = Dash(__name__)

app.layout = html.Div([
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                dbc.Col([
                    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.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.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.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'})
                ]),
                dbc.Col([
                    html.P("Option 5"),
                        html.Div([
                                dcc.Dropdown(id='option5_dropdown',
                                                 options=option5_list,
                                                 value=[],
                                                 placeholder='All',
                                                 multi=True,
                                                 clearable=True),
                                ],
                                style={'width': '100%', 'display': 'inline-block'})
                ]),
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                    html.Div([
                        html.Div(id='dd-output-container')
                    ])
            ], align='center'),
        ]), color='dark'
    ),
    dbc.Card(
        dbc.CardBody([
            dbc.Row([
                    html.Div([
                        dash_table.DataTable(
                            id='table_container',
                            data=df.to_dict('records')
                        )
                    ])
            ], align='center'),
        ]), color='dark'
    )
])

@app.callback(
    Output('table_container', 'data'),
    [Input('option1_dropdown', 'value'),
     Input('option2_dropdown', 'value'),
     Input('option3_dropdown', 'value'),
     Input('option4_dropdown', 'value'),
     Input('option5_dropdown', 'value')
     ])
 
def set_dropdown_options(value1, value2, value3, value4, value5):
    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
    if not value5 or value5 == 'All':
        value5 = option5_list
    
    ddf = df.query('option1 == @value1 and '
                   'option2 == @value2 and '
                   'option3 == @value3 and '
                   'option4 == @value4 and '
                   'option5 == @value5',
                   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'),
     Input('option5_dropdown', 'value')
     ])

def selection(value1, value2, value3, value4, value5):
    #  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
    if not value5 or value5 == 'All':
        value5 = option5_list
    
    ddf = df.query('option1 == @value1 and '
                   'option2 == @value2 and '
                   'option3 == @value3 and '
                   'option4 == @value4 and '
                   'option5 == @value5',
                   engine='python')    

    return 

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

Solution

  • That can be accomplished with plotly dash. The trick is to use or operators between the first three filters to make them additive and and operators between the last two to make them subtractive. The first three options need to be resolved as one block before involving the last two options.

    Example query structure: (option1 == @value1 or option2 == @value2 or option3 == @value3) and option4 == @value4 and option5 == @value5

    I chose to build the query programmatically based on which filters had values to make the or logic work correctly. See the construct_query() function in the example below.

    def construct_query(filter_values):
        additive_clauses = list()
        subtractive_clauses = list()
    
        for i, filter_value in enumerate(filter_values):
            if filter_value and filter_value != "All":
                clause = f"option{i + 1} == @value{i + 1}"
                if i <= 3:
                    additive_clauses.append(clause)
                else:
                    subtractive_clauses.append(clause)
    
        if len(additive_clauses) > 0 or len(subtractive_clauses) > 0:
            additive_section = " or ".join(additive_clauses)
            subtractive_clauses = " and ".join(subtractive_clauses)
    
            if additive_section and subtractive_clauses:
                query = f"({additive_section}) and {subtractive_clauses}"
            else:
                query = additive_section or subtractive_clauses
    
            return query
    

    Another challenge is to avoid creating circular callbacks that have the same input and output components. One way to accomplish this is to break large callbacks into multiple separate callbacks so that the inputs and outputs aren't circular. In the example below, I separated the updating of the first three dropdowns into update_additive_options() and the last two dropdowns into update_subtractive_options(). Plotly also describes another way to manage circular callbacks in their advanced-callbacks docs with the context functionality.

    Example i: Image of results from example i Example ii: Image of results from example ii

    Here is the full version of my code:

    import pandas as pd
    from dash import Dash, dcc, html, Input, Output, dash_table
    import dash_bootstrap_components as dbc
    from itertools import cycle
    import random
    
    Year = cycle(["2012", "2013", "2014", "2015"])
    Season = cycle(["Win", "Spr", "Sum", "Fall"])
    Month = cycle(
        ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
    )
    temp_group = cycle(["Hot", "Cold", "Mild"])
    prec_group = cycle(["Dry", "Wet"])
    
    df = pd.DataFrame(index=range(20))
    
    df["option1"] = [next(Year) for count in range(df.shape[0])]
    df["option2"] = [next(Season) for count in range(df.shape[0])]
    df["option3"] = [next(Month) for count in range(df.shape[0])]
    df["option4"] = [next(temp_group) for count in range(df.shape[0])]
    df["option5"] = [next(prec_group) for count in range(df.shape[0])]
    
    option1_list = sorted(df["option1"].unique().tolist())
    option2_list = df["option2"].unique().tolist()
    option3_list = df["option3"].unique().tolist()
    option4_list = sorted(df["option4"].unique().tolist())
    option5_list = sorted(df["option5"].unique().tolist())
    
    app = Dash(__name__)
    
    app.layout = html.Div(
        [
            dbc.Card(
                dbc.CardBody(
                    [
                        dbc.Row(
                            [
                                dbc.Col(
                                    [
                                        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.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.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.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",
                                            },
                                        ),
                                    ]
                                ),
                                dbc.Col(
                                    [
                                        html.P("Option 5"),
                                        html.Div(
                                            [
                                                dcc.Dropdown(
                                                    id="option5_dropdown",
                                                    options=option5_list,
                                                    value=[],
                                                    placeholder="All",
                                                    multi=True,
                                                    clearable=True,
                                                ),
                                            ],
                                            style={
                                                "width": "100%",
                                                "display": "inline-block",
                                            },
                                        ),
                                    ]
                                ),
                            ],
                            align="center",
                        ),
                    ]
                ),
                color="dark",
            ),
            dbc.Card(
                dbc.CardBody(
                    [
                        dbc.Row(
                            [html.Div([html.Div(id="dd-output-container")])], align="center"
                        ),
                    ]
                ),
                color="dark",
            ),
            dbc.Card(
                dbc.CardBody(
                    [
                        dbc.Row(
                            [
                                html.Div(
                                    [
                                        dash_table.DataTable(
                                            id="table_container", data=df.to_dict("records")
                                        )
                                    ]
                                )
                            ],
                            align="center",
                        ),
                    ]
                ),
                color="dark",
            ),
        ]
    )
    
    
    def construct_query(filter_values):
        additive_clauses = list()
        subtractive_clauses = list()
    
        for i, filter_value in enumerate(filter_values):
            if filter_value and filter_value != "All":
                clause = f"option{i + 1} == @value{i + 1}"
                if i <= 3:
                    additive_clauses.append(clause)
                else:
                    subtractive_clauses.append(clause)
    
        if len(additive_clauses) > 0 or len(subtractive_clauses) > 0:
            additive_section = " or ".join(additive_clauses)
            subtractive_clauses = " and ".join(subtractive_clauses)
    
            if additive_section and subtractive_clauses:
                query = f"({additive_section}) and {subtractive_clauses}"
            else:
                query = additive_section or subtractive_clauses
    
            return query
    
    
    @app.callback(
        [
            Output("option1_dropdown", "options"),
            Output("option2_dropdown", "options"),
            Output("option3_dropdown", "options"),
        ],
        [
            Input("option4_dropdown", "value"),
            Input("option5_dropdown", "value"),
        ],
    )
    def update_additive_options(value4, value5):
    
        query = None
    
        option4_query = "option4 == @value4"
        option5_query = "option5 == @value5"
    
        if value4 and value4 != "All" and value5 and value5 != "All":
            query = f"{option4_query} and {option5_query}"
        elif value4 and value4 != "All":
            query = option4_query
        elif value5 and value5 != "All":
            query = option5_query
    
        if query:
            df_filtered = df.query(
                query,
                engine="python",
            )
        else:
            df_filtered = df
    
        return (
            sorted(df_filtered["option1"].unique().tolist()),
            df_filtered["option2"].unique().tolist(),
            df_filtered["option3"].unique().tolist(),
        )
    
    
    @app.callback(
        [Output("option4_dropdown", "options"), Output("option5_dropdown", "options")],
        [
            Input("option1_dropdown", "value"),
            Input("option2_dropdown", "value"),
            Input("option3_dropdown", "value"),
        ],
    )
    def update_subtractive_options(value1, value2, value3):
    
        query = None
        additive_clauses = []
        for i, filter_value in enumerate([value1, value2, value3]):
            if filter_value and filter_value != "All":
                clause = f"option{i + 1} == @value{i + 1}"
                additive_clauses.append(clause)
    
        if len(additive_clauses) > 0:
            query = " or ".join(additive_clauses)
    
        if query:
            df_filtered = df.query(
                query,
                engine="python",
            )
        else:
            df_filtered = df
    
        return (
            sorted(df_filtered["option4"].unique().tolist()),
            sorted(df_filtered["option5"].unique().tolist()),
        )
    
    
    @app.callback(
        Output("table_container", "data"),
        [
            Input("option1_dropdown", "value"),
            Input("option2_dropdown", "value"),
            Input("option3_dropdown", "value"),
            Input("option4_dropdown", "value"),
            Input("option5_dropdown", "value"),
        ],
    )
    def update_table(value1, value2, value3, value4, value5):
    
        query = construct_query(filter_values=[value1, value2, value3, value4, value5])
    
        if query:
            df_filtered = df.query(
                query,
                engine="python",
            )
        else:
            df_filtered = df
    
        return df_filtered.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"),
            Input("option5_dropdown", "value"),
        ],
    )
    def selection(value1, value2, value3, value4, value5):
        #  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
        if not value5 or value5 == "All":
            value5 = option5_list
    
        ddf = df.query(
            "option1 == @value1 and "
            "option2 == @value2 and "
            "option3 == @value3 and "
            "option4 == @value4 and "
            "option5 == @value5",
            engine="python",
        )
    
        return
    
    
    if __name__ == "__main__":
        app.run_server(debug=True, dev_tools_hot_reload=False)