Pattern matching dropdown update

Hi,
I’m discovering dash (mainly because plotly parallel coordinates are the best I’ve found in Python, thanks for that !) and trying to use it as the gui for a simple data exploration tool. I ran into an error while trying to define a callback to update the options of an arbitrary number of dropdown items, thanks to pattern matching callbacks. Dash is sending me an error if I try click on the update dropdown button if one or more plot block (containing the dropdowns) has been added. The error is different depending on the number of plot blocks defined :

callback_id, i, len(vi), len(outi), repr(outi), repr(vi)
dash.exceptions.InvalidCallbackReturnValue: Invalid number of output values for ..{"index":["ALL"],"type":"var_x_dropdown"}.options...{"index":["ALL"],"type":"var_y_dropdown"}.options...{"index":["ALL"],"type":"var_z_dropdown"}.options.. item 0.
Expected 2, got 1
output spec: [{'id': {'index': 4, 'type': 'var_x_dropdown'}, 'property': 'options'}]
output value: [{'disp': 'vartest3 (unit)', 'value': 'vartest3'}, {'disp': 'vartest4 (unit)', 'value': 'vartest4'}]

From my understanding, dash is not happy with my return statement, but I don’t understand why, any help would be appreciated :slight_smile:
My sample code is the following :

# -*- coding: utf-8 -*-

import plotly.io as pio

import dash
import dash_core_components as dcc
import dash_html_components as html

from dash.dependencies import Input, Output, State, MATCH, ALL
from dash.exceptions import PreventUpdate

# Lists as placeholder, in the real program those lists are defined
# from an uploaded Excel file
temp_list1 = [{"disp":"vartest1 (unit)", "value":"vartest1"},
             {"disp":"vartest2 (unit)", "value":"vartest2"}]
temp_list2 = [{"disp":"vartest3 (unit)", "value":"vartest3"},
             {"disp":"vartest4 (unit)", "value":"vartest4"}]

# Dynamic bloc designed to hold plot settings and the plot itself.
# Multiple "instances" might be created by the user
def def_div_scatter_plot(id_index):
    
    var_options = [{'value' : var['disp'], 'label' : var['disp']}
                   for var in temp_list1]
    div = html.Div(
        id={'type': 'graph_div',
            'index': id_index},
        children=[
            html.Div(
                id={'type': 'graph_div_left',
                    'index': id_index},
                children=[
                    html.H3(
                        children='Plot {0} : scatter'.format(id_index),
                    ),
                    dcc.Dropdown(
                        id={'type': 'subsets_dropdown',
                            'index': id_index},
                        options=[],
                        multi=True,
                        placeholder="Select subsets to apply"
                    ),
                    dcc.Dropdown(
                        id={'type': 'var_x_dropdown',
                            'index': id_index},
                        options=var_options,
                        placeholder="X-axis variable"
                    ),
                    dcc.Dropdown(
                        id={'type': 'var_y_dropdown',
                            'index': id_index},
                        options=var_options,
                        placeholder="Y-axis variable"
                    ),
                    dcc.Dropdown(
                        id={'type': 'var_z_dropdown',
                            'index': id_index},
                        options=var_options,
                        placeholder="Z-axis variable (optionnal)"
                    ),
                    html.Button(
                        id={'type': 'graph_del_button',
                            'index': id_index},
                        className='one-half column',
                        children='del graph'
                    )
                ]
            ),
            html.Div(
                id={'type': 'graph_div_right',
                    'index': id_index},
                className='flex-item-50pct',
                children=[
                    html.Div(
                        id={'type': 'graph_div_right',
                        'index': id_index},
                        className='one-half column',
                        children=['test2']
                    )
                ]
            )
        ]
    )
    return div


# My complete application is split into multiple py files, that's why
# I'm defining the callbacks through a function
def define_callbacks(app):
    
    # Add or remove plot blocs
    @app.callback(
        Output('graphs_container', 'children'),
        [
        Input('add_scatterPlot_button', 'n_clicks'),
        Input({'type': 'graph_del_button', 'index': ALL}, 'n_clicks')
        ],
        [
        State('graphs_container', 'children')
        ]
    )
    def manage_graphs(n_clicks_scatter, n_clicks_rm, current_graphs):
        
        # Context and init handling (no action)
        ctx = dash.callback_context
        if not ctx.triggered :
            raise dash.exceptions.PreventUpdate
        button_id = ctx.triggered[0]['prop_id'].split('.')[0]
                 
        # Creation of a new graph
        if button_id in ('add_scatterPlot_button',
                         'add_parCoorPlot_button'):
            # ID index definition
            if n_clicks_scatter is None :
                n_scatter = 0
            else:
                n_scatter = n_clicks_scatter
            id_index = n_scatter
             # new graph creation
            if button_id == 'add_scatterPlot_button':
                subset_graph = def_div_scatter_plot(id_index)
                return current_graphs + [subset_graph]
     
        # Removal of an existing graph
        else:
            graph_id_to_remove = eval(button_id)['index']
            return [gr for gr in current_graphs
                    if gr['props']['id']['index'] != graph_id_to_remove]
        
        
    # update dropdown options --> NOT WORKING PROPERLY
    @app.callback(
        [
        Output({'type': 'var_x_dropdown', 'index': ALL}, 'options'),
        Output({'type': 'var_y_dropdown', 'index': ALL}, 'options'),
        Output({'type': 'var_z_dropdown', 'index': ALL}, 'options')
        ],
        [
        Input('update_dropdown', 'n_clicks')
        ]
    )
    def update_div_excel_disp(nclicks): 
        if nclicks is None:
            raise dash.exceptions.PreventUpdate
        else:
            var_options_graphs = temp_list2
            ctx = dash.callback_context
            print('\n!!!!!\n {} \n!!!!!\n'.format(ctx.triggered))
        return (var_options_graphs,
                var_options_graphs,
                var_options_graphs)
        
    return


def main():
    pio.renderers.default='browser'
    app = dash.Dash(__name__)
    app.layout = html.Div(
        # Titles
        id='main',
        children=[
            html.Div(
            id='graphs',
                children=[
                    html.H2(
                        children='Graphs def',
                    ),
                    html.Button(
                        id='add_scatterPlot_button',
                        children='Add scatter plot'
                    ),
                    html.Button(
                        id='update_dropdown',
                        children='Update dropdown'
                    ),
                    html.Div(
                        id='graphs_container',
                        children=[]
                    )
                ]
        )
        
    ])  
    define_callbacks(app)
    app.run_server(debug=False)
    

if __name__ == "__main__":
    main()

Problem solved. First, I was not sending the correct format for dropdown options definition in my example (that part was correct in my full script though). Second, I had not understood that I should return a list where each item corresponds to one of the dropdown matched by the ALL statement. It does sound logic for most application, so a bit dumb on my part there ! A working and clarified version is available at the end of this post.

I can see that my pattern matching callback is not fired at the server start because no dropdown is defined at that moment. I was not expecting it, but it sounds logic too :slight_smile:

However, I am still wondering why the update callback is fired whenever I create/delete a new dropdown menu, considering its only input is a button action “n_clicks”. When I print “dash.callback_context.triggered” in my callback, I get :

[{'prop_id': '.', 'value': None}]

:face_with_monocle: I would have expected no trigger, or to simply get “None” like at the start of the server when all callbacks are fired. Could somebody explain me why it is acting like that ?

The full updated code :

# -*- coding: utf-8 -*-

import plotly.io as pio
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ALL


# Lists as placeholder, in the real program those lists are defined
# from an uploaded Excel file
var_list1 = ["var_1_not_updated", "var_2_not_updated"]
var_list2 = ["var_1_UPDATED", "var_2_UPDATED"]

app = dash.Dash(__name__)

app.layout = html.Div(
    # Titles
    id='main',
    children=[
        html.Div(
        id='graphs',
            children=[
                html.H2(
                    children='Dropdowns',
                ),
                html.Button(
                    id='add_dropdown_button',
                    children='Add dropdown'
                ),
                html.Button(
                    id='update_dropdowns_button',
                    children='Update all existing dropdowns'
                ),
                html.Div(
                    id='dropdowns_container',
                    children=[]
                )
            ]
        )
    ]
)  

# Add or remove plot blocs in the 'dropdowns_container'
@app.callback(
    Output('dropdowns_container', 'children'),
    [
    Input('add_dropdown_button', 'n_clicks'),
    Input({'type': 'del_dropdown_button', 'index': ALL}, 'n_clicks')
    ],
    [
    State('dropdowns_container', 'children')
    ]
)
def manage_graphs(n_clicks_scatter, n_clicks_rm, current_graphs):
    
    # Context and init handling (no action)
    ctx = dash.callback_context
    if not ctx.triggered :
        raise dash.exceptions.PreventUpdate
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
             
    # Creation of a new graph
    if button_id == 'add_dropdown_button':
        # ID index definition
        if n_clicks_scatter is None :
            index_id = 0
        else:
            index_id = n_clicks_scatter
        # Appending a dropdown block to 'dropdowns_container'children
        subset_graph = html.Div(
            id={'type': 'bloc_div',
                'index': index_id},
            children=[
                html.H3(
                    children='Dropdown {0}'.format(index_id),
                ),
                dcc.Dropdown(
                    id={'type': 'var_dropdown',
                        'index': index_id},
                    options=[{'value' : var, 'label' : var}
                             for var in var_list1],
                    placeholder="Variable"
                ),
                html.Button(
                    id={'type': 'del_dropdown_button',
                        'index': index_id},
                    children='del'
                )
            ],
            style={'border-style': 'solid',
                   'border-width': '1px',
                   'padding': '5px 5px',
                   'margin': '5px 5px'}
        )
        return current_graphs + [subset_graph]
 
    # Removal of an existing graph
    else:
        graph_id_to_remove = eval(button_id)['index']
        return [gr for gr in current_graphs
                if gr['props']['id']['index'] != graph_id_to_remove]
    
    
# update dropdown options --> NOT WORKING PROPERLY
@app.callback(
    Output({'type': 'var_dropdown', 'index': ALL}, 'options'),
    [Input('update_dropdowns_button', 'n_clicks')],
    [State('dropdowns_container', 'children')]
)
def update_div_excel_disp(nclicks, dropdowns):
    # Context and init handling (no action)
    ctx = dash.callback_context
    print(ctx.triggered)
    if not ctx.triggered :
        raise dash.exceptions.PreventUpdate
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    if button_id == "":
        raise dash.exceptions.PreventUpdate
        
    # Dropdown options update
    var_options_graphs = [{'value' : var, 'label' : var}
                          for var in var_list2]
    return [var_options_graphs for i in range(len(dropdowns))]


if __name__ == '__main__':
    pio.renderers.default='browser'
    app.run_server(debug=False)
1 Like

Thanks. for this solution. I was having the same issue.