Cascading dropdowns

Hi I am having issues with a callback I am making in DCC dropdowns.

I have an app that reads in data from a csv file into a pandas dataframe. I then want to analyse that data with Dash/Plotly.

I went through the tutorials again yesterday and the one about the cascading callback. I have 3 dropdowns and this should be used to slice and dice the data. If I select ALL from the Level 1 dropdown then the callback should return me my complete data frame.

If I select an option that is not all ( Operational Risk, Financial Risk, Business & Strategic Risk) then it should propagate those values into my Level 2 Dropdown, and likewise onto level 3. At that point the callback should return me a subset of the data.

As an example here is Financial risk broken down into its there levels

I have 3 dropdowns with a cascade between them. it kind of works if I select the All option, but as soon as I select a category in Level 1 I get a callback error

Traceback (most recent call last):
  File "C:\Data\PythonProjects\clensed\clensed.py", line 680, in set_tl2_options
    for i in sorted(raca_options['risk'].astype(str).unique())]
  File "C:\Data\PythonProjects\clensed\venv\Lib\site-packages\pandas\core\series.py", line 824, in __getitem__
    return self._get_value(key)
  File "C:\Data\PythonProjects\clensed\venv\Lib\site-packages\pandas\core\series.py", line 932, in _get_value
    loc = self.index.get_loc(label)
  File "C:\Data\PythonProjects\clensed\venv\Lib\site-packages\pandas\core\indexes\range.py", line 354, in get_loc
    raise KeyError(key)
KeyError: 'risk'

As soon as I change the level 1 option to another item I get a Key Error updating risk.options.

Here is my code for the callback

@app.callback(
    Output('risk', 'options'),
    Input('risk_types', 'value'))
def set_tl2_options(tl1_options):
    if tl1_options != 'All':
        raca_options = raca_df['risk_types'] == tl1_options
        #print(f'DEBUG1: TL 1 Not equal to all: {raca_options}')
    else:
        raca_options = raca_df
        #print(f'DEBUG2: TL1 equal to all: {raca_options}')

    return [{'label': i, 'value': i}
            for i in sorted(raca_options['risk'].astype(str).unique())]

@app.callback(
    Output('level3', 'options'),
    Input('risk', 'value'))
def set_tl3_options(tl2_options):
    if tl2_options != 'All':
        raca_options = raca_df[raca_df['risk'] == tl2_options]
        #print(f'DEBUG3: TL2 Not equal to all: {raca_options}')
    else:
        raca_options = raca_df
        #print(f'DEBUG4: TL2 equal to all: {raca_options}')
    return [{'label': i, 'value': i}
            for i in sorted(raca_options['level3'].astype(str).unique())]

And here is my Dropdown code.

# ------------------------------------------------------------------------------
# Define dropdowns
# ------------------------------------------------------------------------------
# Risk Category 1
risk_types_dropdown = dcc.Dropdown(
    id='risk_types',
    multi=False,
    value = 'All',
    clearable=False,
    searchable=True,
    persistence=True,
    persistence_type='session',
    style={"width": "100%"},

    options=[{'label': k, 'value': k}
             for k in sorted(raca_df['risk_types'].astype(str).unique())]
            + [{'label': 'All', 'value': 'All'}],
)

# Risk Category 2
risk_dropdown = dcc.Dropdown(
    id='risk',
    multi=False,
    value ='All',
    clearable=False,
    searchable=True,
    placeholder='Select...',
    persistence=True,
    persistence_type='session',
    style={"width": "100%"},

    options=[{'label': k, 'value': k}
             for k in sorted(raca_df['risk'].astype(str).unique())]
)

# Risk Category 2
level3_dropdown = dcc.Dropdown(
    id='level3',
    multi=False,
    value ='All',
    clearable=False,
    searchable=True,
    placeholder='Select...',
    persistence=True,
    persistence_type='session',
    style={"width": "100%"},

    options=[],

My full code and the complete risk breakdown matrix along with test data can be had from my git repo GitHub - twelsh37/clensed: reporting framework utilising dash

And finally, how would I get that chart/data returned data back? Would it come as the value from the callback that I would then feed into a function to make the chart / data table?

All help gratefully received.

Hi Tom,
Try changing the filtering line of code to this:

raca_options = raca_df[raca_df['risk_types'] == tl1_options]

Thanks Adam. I couldn’t sleep so have fixed that now

Ok, “I am so close I can smell the roses.”

I now have my 3 dropdown working and returning data to the data table.
Unfortunately, the data being returned to the data table is not the filtered content it is the whole data frame. Probably because I am returning the whole data frame.

What I really need is if the user selects all on the first drop down then Dropdown 2 and 3 should be disabled and the whole data frame is then shipped to the data table or graph depending on where I want it.

If the user selects a subsection in level 1 dropdown then i should only get the subsection returned after I have drilled down to L3, which is the call back that sends the data to the data table and graphs (graphing not implemented yet)

Updated callback code.

# ------------------------------------------------------------------------------
# Set Callback to define our dropdown boxes
# ------------------------------------------------------------------------------
@app.callback(
    Output('risk', 'options'),
    Input('risk_types', 'value'))
def set_tl2_options(tl1_options):
    if tl1_options != 'All':
        raca_options = raca_df[raca_df['risk_types'] == tl1_options]
        #print(f'DEBUG1: TL 1 Not equal to all: {raca_options}')
        print(f'DEBUG 1.1: L1 options "NOT ALL": {raca_options}')
    else:
        raca_options = raca_df
        #print(f'DEBUG2: TL1 equal to all: {raca_options}')
        print(f'DEBUG 1.2: L1 options "ALL": {raca_options}')
    return [{'label': i, 'value': i}
            for i in sorted(raca_options['risk'].astype(str).unique())]

@app.callback(
    Output('level3', 'options'),
    Input('risk', 'value'))
def set_tl3_options(tl2_options):
    if tl2_options != 'All':
        raca_options = raca_df[raca_df['risk'] == tl2_options]
        print(f'DEBUG 2.1: TL2 Not equal to all: {raca_options}')
    else:
        raca_options = raca_df
        print(f'DEBUG 2.2: TL2 equal to all: {raca_options}')
    return [{'label': i, 'value': i}
            for i in sorted(raca_options['level3'].astype(str).unique())]

# ------------------------------------------------------------------------------
# Define Callback to update data_table  on tab_1 id = table
#------------------------------------------------------------------------------
@app.callback(
    Output('table', 'data'),
    Input('level3', 'value'))
def output_dataframe(data):
    print(f'DEBUG 3.1: Level 3 value {data}')
    return raca_df.to_dict('records')

and here is the data table definition

# ------------------------------------------------------------------------------
# Define Risk Datatable
# ------------------------------------------------------------------------------
data_table = dash_table.DataTable(
    id='table',
    # This line reads in all the columns in our dataframe raca_df
    #columns=[{"name": i, "id": i} for i in raca_df.columns],
    columns=[
        {'name': 'Risk description', 'id': 'risk_description', 'type': 'text',
         'editable': False},
        {'name': 'Risk ID', 'id': 'risk_id', 'type': 'text', 'editable': False},
        {'name': 'Risk Owner', 'id': 'risk_owner', 'type': 'text',
         'editable': False},
        {'name': 'Risk(Title)', 'id': 'risk_title', 'type': 'text',
         'editable': False},
        {'name': 'Risk Category 1', 'id': 'risk_types', 'type': 'text',
         'editable': False},
        {'name': 'Risk Category 2', 'id': 'risk', 'type': 'text',
         'editable': False},
        {'name': 'Risk Category 3', 'id': 'level3', 'type': 'text',
         'editable': False},
        {'name': 'Gross Risk', 'id': 'gross_risk', 'type': 'numeric',
         'editable': False},
        {'name': 'Net Risk', 'id': 'net_risk', 'type': 'numeric',
         'editable': False},
    ],
    data =[],
    #data=raca_df.to_dict('records'),
    filter_action="native",
    sort_action="native",
    style_cell={
        'overflow': 'hidden',
        'textOverflow': 'ellipsis',
        'maxWidth': 0,
        'textAlign': 'left',
        'fontSize': 12,
        'font-family':'sans-serif',
    },

The updated codebase is all available on github at - https://github.com/twelsh37/clensed

So I have worked out part of the above
To address this point I changed to a hide the dropdowns completely.
You have to wrap tye dropdowns in a Div so you can turn them on and off ( IT will love that)
This was based on a stackoverflow [answer](python - Plotly Dash: Hide/show slider component by updating different dropdown component - Stack Overflow hide-show-dash-slider-component-by-updating-different-dropdown-component).

# ------------------------------------------------------------------------------
# Callback to hide L2 and L3 dropdown boxes if risk_type == 'ALL'
# ------------------------------------------------------------------------------
# https://stackoverflow.com/questions/62788398/
# hide-show-dash-slider-component-by-updating-different-dropdown-component
@app.callback(
    Output('dropdown-container', 'style'),
    [Input('risk_types', 'value')])
def show_hide_element(visibility_state):
    if visibility_state == 'All':
        return {'display': 'none'}
    else:
        return {'display': 'block'

Still stuck on the rest though.