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:
A default of all values should always be the initial starting point
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).
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)
Edit 2:
Is there a way to include the original column names without converting to using an integer suffix?
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["Year"] = [next(Year) for count in range(df.shape[0])]
df["Season"] = [next(Season) for count in range(df.shape[0])]
df["Month"] = [next(Month) for count in range(df.shape[0])]
df["Temp"] = [next(temp_group) for count in range(df.shape[0])]
df["Prec"] = [next(prec_group) for count in range(df.shape[0])]
Year_list = sorted(df["Year"].unique().tolist())
Season_list = df["Season"].unique().tolist()
Month_list = df["Month"].unique().tolist()
Temp_list = sorted(df["Temp"].unique().tolist())
Prec_list = sorted(df["Prec"].unique().tolist())
df = df.rename(columns = {'Year':'option1',
'Season':'option2',
'Month':'option3',
'Temp':'option4',
'Prec':'option5'})
app = Dash(__name__)
app.layout = html.Div(
[
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[
dbc.Col(
[
html.P("Year"),
html.Div(
[
dcc.Dropdown(
id="Year_dropdown",
options=Year_list,
value=[],
placeholder="All",
multi=True,
clearable=True,
),
],
style={
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Season"),
html.Div(
[
dcc.Dropdown(
id="Season_dropdown",
options=Season_list,
value=[],
placeholder="All",
multi=True,
clearable=True,
),
],
style={
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Month"),
html.Div(
[
dcc.Dropdown(
id="Month_dropdown",
options=Month_list,
value=[],
placeholder="All",
multi=True,
clearable=True,
),
],
style={
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Temp"),
html.Div(
[
dcc.Dropdown(
id="Temp_dropdown",
options=Temp_list,
value=[],
placeholder="All",
multi=True,
clearable=True,
),
],
style={
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Prec"),
html.Div(
[
dcc.Dropdown(
id="Prec_dropdown",
options=Prec_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",
),
]
)
df = df.rename(columns = {'Year':'option1',
'Season':'option2',
'Month':'option3',
'Temp':'option4',
'Prec':'option5'})
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("Year_dropdown", "options"),
Output("Season_dropdown", "options"),
Output("Month_dropdown", "options"),
],
[
Input("Temp_dropdown", "value"),
Input("Prec_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("Temp_dropdown", "options"), Output("Prec_dropdown", "options")],
[
Input("Year_dropdown", "options"),
Input("Season_dropdown", "options"),
Input("Month_dropdown", "options"),
],
)
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("Year_dropdown", "value"),
Input("Season_dropdown", "value"),
Input("Month_dropdown", "value"),
Input("Temp_dropdown", "value"),
Input("Prec_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("Year_dropdown", "value"),
Input("Season_dropdown", "value"),
Input("Month_dropdown", "value"),
Input("Temp_dropdown", "value"),
Input("Prec_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 = Year_list
if not value2 or value2 == "All":
value2 = Season_list
if not value3 or value3 == "All":
value3 = Month_list
if not value4 or value4 == "All":
value4 = Temp_list
if not value5 or value5 == "All":
value5 = Prec_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)
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.
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)