Syncing values between dependent selectors

I have three chained selectors: dropdown → checklist#1 → checklist#2. Selecting an item on the dropdown menu causes checklist #1 to populate. Checking an item on checklist #1 causes checklist #2 to populate.

My question: How do I write my code so that a checkbox that has been checked on checklist #2 is automatically unchecked if the associated parent checkbox on checklist #1 is unchecked?

e.g.:

  1. I select Item #1 on dropdown menu. Checklist #1 populates with “Subitem #1a,” “Subitem #1b,” and “Subitem #1c.”
  2. I select Subitem #1a in Checklist #1-> Checklist #2 populates with [1a, 2a, 3a]
  3. I select Subitem #1c in Checklist #1-> Checklist #2 becomes [1a, 2a, 3a, 1c, 2c, 3c]
  4. I select 1a and 3c in Checklist #2.
  5. I uncheck Subitem #1a in Checklist #1 → I want ‘1a’ in Checklist #2 to also automatically be unselected.

The result in (5) is what I am having trouble with. I can get the options to disappear if I uncheck the parent item, but I cannot figure out how to also automatically clear the value when I uncheck the parent item.

See below code for reproduceable example (using School District (dropdown) - > Schools (checklist #1) → Grades (checklist #2). I want to prevent grades from being displayed (on a graph) if none of the checked schools (in checklist #1) actually have those grades.

Hope this makes sense.

from dash import Dash, dcc, html, Input, Output

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]

app = Dash(__name__, external_stylesheets=external_stylesheets)

dropdown_items = {
    'School District #1': ['School #1A', 'School #1B', 'School #1C'],
    'School District #2': ['School #2A', 'School #2B', 'School #2C'],
    'School District #3': ['School #3A', 'School #3B', 'School #3C']
}

app.layout = html.Div(
    [
    dcc.Dropdown(
        id='main-dropdown',
        multi = False,
        options = [{'label': k, 'value': k} for k in dropdown_items.keys()],
        value = 'School District #1',
    ),
    dcc.Checklist(id="select-checklist", inline=True),
    dcc.Checklist(id="grades-checklist", inline=True)
    ],
    style={'width': '50%'}
)

# Populate School Checklist from Dropdown
@app.callback(
    Output('select-checklist', 'options'),
    Input('main-dropdown', 'value')
)
def set_select_checklist(dropdown_value):
    return [{'label': i, 'value': i} for i in dropdown_items[dropdown_value]]
     
# Populate Grades Checklist from School Checklist
@app.callback(
    Output("grades-checklist", "options"),
    Input("select-checklist", "value"),
    prevent_initial_call=True,
)
def display(select):
    grade_range = []
    if select:

        if any('School #1' in string for string in select):
            grade_range.append(3)
        
        if any('School #2' in string for string in select):
            grade_range.append(6)
        
        if any('School #3' in string for string in select):
            grade_range.append(9)

        grade_range = list(range(min(grade_range)-2,max(grade_range)+1))

        options = [str(x) for x in grade_range]

    else:
        options=[]    
        
    return options

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

Hi,

Just to clarify:

Do you want to simply unselect 1a or actually remove the options (and uncheck them, of course) added in the step 2?

I will assume you want to remove them from the options. My suggestion would be to set the “options” and “value” for checklist #2 in the same callback, using its previous value as state. Something like:

@app.callback(
    Output("grades-checklist", "options"),
    Output("grades-checklist", "value"),
    Input("select-checklist", "value"),
    State("grades-checklist", "value"),
    prevent_initial_call=True,
)
def display(select, current_selection):
    grade_range = []
    if select:

        if any('School #1' in string for string in select):
            grade_range.append(3)
        
        if any('School #2' in string for string in select):
            grade_range.append(6)
        
        if any('School #3' in string for string in select):
            grade_range.append(9)

        grade_range = list(range(min(grade_range)-2,max(grade_range)+1))

        options = [str(x) for x in grade_range]
        
        # Keep selected values 
        # NOTE you might have to check if current_selection is None
        value = [val for val in current_selection if val in options]

    else:
        options=[]    
        value=[]
        
    return options, value

Please note that this should work in the simple case where the graph you referred to is triggered by checklist #2 only, not by changes in checklist #1. Otherwise it can be a bit more complicated and you would probably need to use the callback context.

Hope this helps!

Thank you, this helps! I had to tweak your code a bit to get it working (I think you accidentally typed State twice, and you were correct that I needed to account for situations when current_selected was null). I also realized that I needed to account for the case where the grade range being displayed was over-inclusive (e.g, no school with grade ranges 4,5,6 was selected, but 4,5,6 still displayed because of how I was calculating range). The fix is ugly, but more accurately represents the actual code.

What I ended up with is:

@app.callback(
    Output("grades-checklist", "options"),
    Output("grades-checklist","value"),
    Input("select-checklist", "value"),
    State("grades-checklist", "value"),
    prevent_initial_call=True,
)
def display(select, current_selection):
    grade_range = []
    
    if select:

        if any('School #1' in string for string in select):
            grade_range.append(1)
            grade_range.append(2)
            grade_range.append(3)
        
        if any('School #2' in string for string in select):
            grade_range.append(4)
            grade_range.append(5)
            grade_range.append(6)
        
        if any('School #3' in string for string in select):
            grade_range.append(7)
            grade_range.append(8)
            grade_range.append(9)

        grade_range.sort()

        options = [str(x) for x in grade_range]
        
        if current_selection:
            value = [val for val in current_selection if val in options]
        else:
            value=[]
    else:
        options=[]    
        value=[]
        
    return options, value

However, I actually do need both the values in Checklist #1 and Checklist #2 to trigger the graph. There are actually eight selectors involved in getting the data necessary to draw one bar-chart and the complete data set for each school has several hundred possible combinations of data.

I have spent lots of time with callback_context without success. I think my primary issue is that while I understand how to ‘access’ the data, I’m not sure I understand how to ‘use’ it to programmatically modify a selector’s state.

Any additional thoughts would be appreciated!

Indeed I copy-and-pasted too fast, glad you figure it out. :slight_smile:

The callback context can be a bit daunting and I would be happy to assist with specific questions. That said, given that any update in “value” for checklist #1 updates “value” for checklist #2, I don’t think you will need both as triggers in another callback. It should be enough to use “value” from checklist #1 as a State instead of Input.