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
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()