Dash: Working around a Circular Dependency in a Dropdown Menu

Here is a MRE of a dropdown I am trying to create for a dash app. The idea is that the dropdown has 10 items. When the app is run, 2 items are selected by default. The user can click up to 3 more items for a total of 5 items. When 5 items are reached, an error message appears (‘Limit Reached’) and the remaining dropdown items a greyed out (disabled). Once the user deselects one or more items, the remaining dropdown items are no longer disabled.

Problem: The only way I have been able to get this to work results in a Circular Dependency. Is there a way to construct this dropdown without the Circular Dependency or should I just suppress the error message?

import dash
import pandas as pd
from dash import Input, Output, html, dcc

app = dash.Dash(__name__)

app.layout = html.Div(
            [
                html.Div(
                    [
                        html.P('Add or Remove Schools:'),
                        dcc.Dropdown(
                            id='comparison-dropdown',
                            style={'fontSize': '85%'},
                            multi = True,
                            clearable = False,
                            className='dcc_control'
                        ),
                        html.Div(id='input-warning'),
                    ],
                ),

            ],
        )

# create dropdown
@app.callback(
    Output('comparison-dropdown', 'options'),
    Output('input-warning','children'),
    Input('comparison-dropdown', 'value'),
)
def set_dropdown_options(selected):

    data = {'Name': ['School#1', 'School#2', 'School#3', 'School#4','School#5', 'School#6', 'School#7', 'School#8','School#9', 'School#10'],
            'Id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}

    df = pd.DataFrame(data)
    df_dict = dict(zip(df['Name'], df['Id']))
    list = dict(df_dict.items())
  
    default_options = [{'label':name,'value':id} for name, id in list.items()]

    options = default_options
    input_warning = None

    if selected is not None:
        if len(selected) > 4:
            input_warning = html.P(
                id='input-warning',
                children='Limit reached (Maximum of 5 schools).',
            )
            options = [
                {"label": option["label"], "value": option["value"], "disabled": True}
                for option in default_options
            ]

    return options, input_warning

# Set default dropdown options
@app.callback(
    Output('comparison-dropdown', 'value'),
    Input('comparison-dropdown', 'options'),
    Input('comparison-dropdown', 'value'),
)
def set_dropdown_value(options, selections):
    if selections is None:
        default_num = 2
        selections = [d['value'] for d in options[:default_num]]
        return selections
    else:
        return selections

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

Hello @etonblue,

You dont need the second callback, at least when I tested it. :slight_smile:

1 Like

@jinnyzor Thanks! Your answer wasn’t exactly what I was looking for, because while removing the second callback got rid of the circular dependency error, it also removed the default values,. However, it did point me to the full answer, which was removing the second callback and moving the logic setting the default value and adding the Output for dropdown-values to the first callback.

Final working code for a multi-selection dropdown menu with both default values and a limitation on the total number of values selected:

import dash
import pandas as pd
from dash import Input, Output, html, dcc

app = dash.Dash(__name__)

app.layout = html.Div(
            [
                html.Div(
                    [
                        html.P('Add or Remove Schools:'),
                        dcc.Dropdown(
                            id='comparison-dropdown',
                            style={'fontSize': '85%'},
                            multi = True,
                            clearable = False,
                            className='dcc_control'
                        ),
                        html.Div(id='input-warning'),
                    ],
                ),

            ],
        )

@app.callback(
    Output('comparison-dropdown', 'options'),
    Output('input-warning','children'),
    Output('comparison-dropdown', 'value'),
    Input('comparison-dropdown', 'value'),
)
def set_dropdown_options(selected):

    data = {'Name': ['School#1', 'School#2', 'School#3', 'School#4','School#5', 'School#6', 'School#7', 'School#8','School#9', 'School#10'],
            'Id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}

    df = pd.DataFrame(data)
    df_dict = dict(zip(df['Name'], df['Id']))
    list = dict(df_dict.items())
  
    default_options = [{'label':name,'value':id} for name, id in list.items()]

    options = default_options
    input_warning = None
    default_selections = 2
    max_selections = 4

    if selected is None:
        selected = [d['value'] for d in options[:default_num]]

    else:
        if len(selected) > max_selections:
            input_warning = html.P(
                id='input-warning',
                children='Limit reached (Maximum of 5 schools).',
            )
            options = [
                {"label": option["label"], "value": option["value"], "disabled": True}
                for option in default_options
            ]
    
    return options, input_warning, selected

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

Ah, I didnt catch that there were default options and values.

Glad you got it figured out. :slight_smile:

1 Like