Preventing chained callbacks from switching dropdown to original value dash

Viewed 1061

I have a lengthy section of code for several chained callbacks that stem from a multiple nested dictionary. I have all of the necessary dropdowns and options that I would like to provide. However, whenever I change the 'crop' dropdown in this example to something other than the original option (which is corn) it resets the 'Weighting' dropdown below. Similarly, if I change the 'Weighting' dropdown, it resets the 'Forecast Variable' dropdown to the original option. How can I prevent this? The point of the chained callbacks was so that changing one option would change the data that is plotted, as they are all linked.

I don't think the data is important here? But it functions like this:

final_dict[init_date][model][weight][crop]

The above exact dictionary then would output a dataframe. The columns in the dataframe then would be the 'forecast variable' which will eventually be plotted. If I do need to add data I can try and do that but the dict is VERY big.

Here is the code I have so far. Notice that the graph is empty because I haven't gotten that far yet.

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
from pandas import Timestamp
import plotly.graph_objs as go
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import numpy as np
from plotly.subplots import make_subplots
import plotly.express as px
import pandas as pd
import numpy as np
from datetime import timedelta
import glob
import datetime as dt

import xarray as xr
import os 
from PIL import Image
import time
import random

my_dict={}

for i in np.arange(1,17,1):
    n=random.randint(1,10)
    m=random.randint(1,10)
    data=[[pd.Timestamp('2020-10-06'),n,m],[pd.Timestamp('2020-10-07'),m,n],[pd.Timestamp('2020-10-08'),n,m],[pd.Timestamp('2020-10-09'),m,n]]
    my_dict[i]=pd.DataFrame(data=data, columns=['time', 'Temp','Precip'])

final_dict={'day1':{'model1':{'weight1':{'crop1':my_dict[1], 'crop2':my_dict[2]},
                           'weight2':{'crop1':my_dict[3], 'crop2':my_dict[4]}},
                 
                 'model2':{'weight1':{'crop1':my_dict[5], 'crop2':my_dict[6]},
                           'weight2':{'crop1':my_dict[7], 'crop2':my_dict[8]}}},
         
         'day2':{'model1':{'weight1':{'crop1':my_dict[9], 'crop2':my_dict[10]},
                           'weight2':{'crop1':my_dict[11], 'crop2':my_dict[12]}},
                 
                 'model2':{'weight1':{'crop1':my_dict[13], 'crop2':my_dict[14]},
                           'weight2':{'crop1':my_dict[15], 'crop2':my_dict[16]}}}}

app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])

controls = dbc.Card(
    [   dbc.FormGroup(
            [dbc.Label("Init Date"),
                dcc.Dropdown(
                    id='init_dd',
                    options=[{'label': k, 'value': k} for k in final_dict.keys()],
                    value=list(final_dict.keys())[0],
                    clearable=False,
                ),
            ]
        ),
        dbc.FormGroup(
            [dbc.Label("Model"),
                dcc.Dropdown(
                    id='model_dd',
                    clearable=False,
                ),
            ]
        ), 
        dbc.FormGroup(
            [dbc.Label("Crop"),
                dcc.Dropdown(
                    id='crop_dd',
                    clearable=False,
                ),
            ]
        ),           
        dbc.FormGroup(
            [dbc.Label("Weighting"),
                dcc.Dropdown(
                    id='weight_dd',
                    clearable=False,
                ),
            ]
        ),
        dbc.FormGroup(
            [dbc.Label("Forecast Variable"),
                dcc.Dropdown(
                    id='columns_dd',
                    clearable=False,
                ),
            ]
        ),

    ],
    body=True,
)


app.layout = dbc.Container(
    [
        html.Hr(),
        dbc.Row([
            dbc.Col([
                dbc.Row([
                    dbc.Col(controls)
                ],  align="start"), 
            ],xs = 2)
            ,
            dbc.Col([
                dbc.Row([
                    dbc.Col([html.Div(id = 'plot_title')],)
                ]),
                dbc.Row([
                    dbc.Col(dcc.Graph(id="crop-graph")),
                ])
            ])
        ],), 
    ],
    fluid=True,
)
    
# Callbacks #####################################################################
#set the model
@app.callback(
    Output('model_dd', 'options'),
    [Input('init_dd', 'value')])
def set_model_options(model):
    return [{'label': i.replace('_',' '), 'value': i} for i in final_dict[model]]
 
@app.callback(
    Output('model_dd', 'value'),
    [Input('model_dd', 'options')])
def set_model_options_value(available_model):
    return available_model[0]['value']

#set the weight
@app.callback(
    Output('weight_dd', 'options'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value')])
def set_weight_options(selected_init, selected_model):
    return [{'label': i, 'value': i} for i in final_dict[selected_init][selected_model]]
 
@app.callback(
    Output('weight_dd', 'value'),
    [Input('weight_dd', 'options')])
def set_weight_value(available_weight):
    return available_weight[0]['value']

#set the crop
@app.callback(
    Output('crop_dd', 'options'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value'),
     Input('weight_dd', 'value')])
def set_crop_options(selected_init, selected_model, selected_weight):
    return [{'label': i, 'value': i} for i in final_dict[selected_init][selected_model][selected_weight]]
 
@app.callback(
    Output('crop_dd', 'value'),
    [Input('crop_dd', 'options')])
def set_crop_value(available_crop):
    return available_crop[0]['value']

#set the variable
@app.callback(
    Output('columns_dd', 'options'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value'),
     Input('weight_dd', 'value'),
     Input('crop_dd', 'value')])
def set_column_options(selected_init, selected_model, selected_weight, selected_crop):
    return [{'label': i, 'value': i} for i in final_dict[selected_init][selected_model][selected_weight][selected_crop].columns[1:]]
 
@app.callback(
    Output('columns_dd', 'value'),
    [Input('columns_dd', 'options')])
def set_column_value(available_column):
    return available_column[1]['value']

app.run_server(mode='external', port = 8099)   

Edit: Added in sample dummy data. Notice how when changing certain combinations of options, other options switch back to the original value. Would like to prevent that from happening.

2 Answers

The specific data example helped. I see that

  • datasets are stored in nested dictionary
  • you want to allow the user to select a particular dataset (for which each user-input option depends on the previous/upstream selections in the nested structure).
  • Because the nested structure here means that for a given input change, you want to update the input options only for the subsequent/downstream inputs.

About your issue with better controlling chained callbacks, I think it's a matter of using Input() and State() in right places.

Try this (I renamed your final_dict so that it is easier to monitor what's going on):

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
from pandas import Timestamp
import plotly.graph_objs as go
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
import numpy as np
from plotly.subplots import make_subplots
import plotly.express as px
import pandas as pd
import numpy as np
from datetime import timedelta
import glob
import datetime as dt

import xarray as xr
import os 
from PIL import Image
import time
import random

my_dict={}

for i in np.arange(1,17,1):
    n=random.randint(1,10)
    m=random.randint(1,10)
    data=[[pd.Timestamp('2020-10-06'),n,m],[pd.Timestamp('2020-10-07'),m,n],[pd.Timestamp('2020-10-08'),n,m],[pd.Timestamp('2020-10-09'),m,n]]
    my_dict[i]=pd.DataFrame(data=data, columns=['time', 'Temp','Precip'])

final_dict={'day1':{'model1':{'weight1':{'crop1':my_dict[1], 'cropA':my_dict[2]},
                           'weight2':{'crop2':my_dict[3], 'cropB':my_dict[4]}},
                 
                 'model2':{'weight3':{'crop3':my_dict[5], 'cropC':my_dict[6]},
                           'weight4':{'crop4':my_dict[7], 'cropD':my_dict[8]}}},
         
         'day2':{'model3':{'weight5':{'crop5':my_dict[9], 'cropE':my_dict[10]},
                           'weight6':{'crop6':my_dict[11], 'cropF':my_dict[12]}},
                
                 'model4':{'weight7':{'crop7':my_dict[13], 'cropG':my_dict[14]},
                           'weight8':{'crop8':my_dict[15], 'cropH':my_dict[16]}}}}

app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])

controls = dbc.Card(
    [   dbc.FormGroup(
            [dbc.Label("Init Date"),
                dcc.Dropdown(
                    id='init_dd',
                    options=[{'label': k, 'value': k} for k in final_dict.keys()],
                    value=list(final_dict.keys())[0],
                    clearable=False,
                ),
            ]
        ),
        dbc.FormGroup(
            [dbc.Label("Model"),
                dcc.Dropdown(
                    id='model_dd',
                    clearable=False,
                ),
            ]
        ), 
        dbc.FormGroup(
            [dbc.Label("Crop"),
                dcc.Dropdown(
                    id='crop_dd',
                    clearable=False,
                ),
            ]
        ),           
        dbc.FormGroup(
            [dbc.Label("Weighting"),
                dcc.Dropdown(
                    id='weight_dd',
                    clearable=False,
                ),
            ]
        ),
        dbc.FormGroup(
            [dbc.Label("Forecast Variable"),
                dcc.Dropdown(
                    id='columns_dd',
                    clearable=False,
                ),
            ]
        ),

    ],
    body=True,
)


app.layout = dbc.Container(
    [
        html.Hr(),
        dbc.Row([
            dbc.Col([
                dbc.Row([
                    dbc.Col(controls)
                ],  align="start"), 
            ],xs = 2)
            ,
            dbc.Col([
                dbc.Row(html.Div(id='selected_data')),
                # dbc.Row([
                #     dbc.Col([html.Div(id = 'plot_title')],)
                # ]),
                dbc.Row([
                    dbc.Col(dcc.Graph(id="crop-graph")),
                ])                
            ])
        ],), 
    ],
    fluid=True,
)
    
# Callbacks #####################################################################
#set the model
@app.callback(
    Output('model_dd', 'options'),
    [Input('init_dd', 'value')])
def set_model_options(model):
    return [{'label': i.replace('_',' '), 'value': i} for i in final_dict.get(model).keys()]
 
@app.callback(
    Output('model_dd', 'value'),
    [Input('model_dd', 'options')])
def set_model_options_value(available_model):
    return available_model[0]['value']

#set the weight
@app.callback(
    Output('weight_dd', 'options'),
    [Input('model_dd', 'value')],
    [State('init_dd', 'value')])
def set_weight_options(selected_model, selected_init):
    if selected_model is None: return None 
    print('selected_model(): ', selected_init, selected_model)
    return [{'label': i, 'value': i} for i in final_dict.get(selected_init).get(selected_model).keys()]
 
@app.callback(
    Output('weight_dd', 'value'),
    [Input('weight_dd', 'options')])
def set_weight_value(available_weight):
    return available_weight[0]['value']

#set the crop
@app.callback(
    Output('crop_dd', 'options'),
     [Input('weight_dd', 'value')],
     [State('init_dd', 'value'),
     State('model_dd', 'value')])
def set_crop_options(selected_weight, selected_init, selected_model):
    if selected_model is None or selected_weight is None: return None 
    print('set_crop_options(): ',selected_init, selected_model, selected_weight)
    return [{'label': i, 'value': i} for i in final_dict.get(selected_init).get(selected_model).get(selected_weight).keys()]

@app.callback(
    Output('crop_dd', 'value'),
    [Input('crop_dd', 'options')])
def set_crop_value(available_crop):
    return available_crop[0]['value']

#set the variable
@app.callback(
    Output('columns_dd', 'options'),
    [Input('crop_dd', 'value')],
    [State('init_dd', 'value'),
     State('model_dd', 'value'),
     State('weight_dd', 'value')])
def set_column_options(selected_crop, selected_init, selected_model, selected_weight):
    if selected_crop is None or selected_weight is None or selected_model is None: return None
    print('set_column_options(): ', selected_init, selected_model, selected_weight, selected_crop)
    return [{'label': i, 'value': i} for i in final_dict.get(selected_init).get(selected_model).get(selected_weight).get(selected_crop).columns[1:]]
 
@app.callback(
    Output('columns_dd', 'value'),
    [Input('columns_dd', 'options')])
def set_column_value(available_column):
    if available_column is None: return None
    return available_column[1]['value']

@app.callback(
    Output('selected_data', 'children'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value'),
     Input('weight_dd', 'value'),
     Input('crop_dd', 'value'),
     Input('columns_dd','value')]
    )
def show_data(init_dd, model_dd, weight_dd, crop_dd, columns_dd):
    if crop_dd is None or weight_dd is None or model_dd is None or columns_dd is None: return None
    print('show_data():', init_dd, model_dd, weight_dd, crop_dd, columns_dd)
    try:
        data = final_dict[init_dd][model_dd][weight_dd][crop_dd][columns_dd].to_json(orient='split')
    except:
        return
    return data


def make_plot(df, var):
    fig = go.Figure(
            data=[go.Scatter(x=df['time'], y=df[var], name=var)],
            layout={
                'yaxis': {'title': f'Plot of <b>{var}</b>'}
            }
        )
    return fig


no_data_fig = {"layout": {
        "xaxis": { "visible": False},
        "yaxis": {"visible": False},
        "annotations": [
            { "text": "",
                "xref": "paper",
                "yref": "paper",
                "showarrow": False,
                "font": {"size": 20 }
            }]
        }
    }

@app.callback(
    Output('crop-graph', 'figure'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value'),
     Input('weight_dd', 'value'),
     Input('crop_dd', 'value'),
     Input('columns_dd','value')]
    )
def plot_data(init_dd, model_dd, weight_dd, crop_dd, columns_dd):
    if crop_dd is None or weight_dd is None or model_dd is None or columns_dd is None: return None
    print('plot_data():', init_dd, model_dd, weight_dd, crop_dd, columns_dd)
    try:
        data = final_dict[init_dd][model_dd][weight_dd][crop_dd]
        data_col = data[columns_dd]
    except:
        return no_data_fig
    return make_plot(data, columns_dd)


app.run_server(mode='external', port = 8098, debug=True)   

Here is an another version.

  • I noticed that you wanted to keep the data column column_dd to be fixed without being updated (maybe assumeing the identical columns in the final dataset across different versions). So, I commented out the callback for updating column_dd.
  • You can also combine outputs as a list.
  • I tried to show a way to use dynamic input generation but ended up not needing to dynamically update it (I couldn't specify the same output-id twice, which was inconvenient. I just kept this for a demo, and you don't need to switch to this style.) Note that it is still possible to use it as a State() in a callback and overwrite its properties).
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
from pandas import Timestamp
import plotly.graph_objs as go
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
import numpy as np
from plotly.subplots import make_subplots
import plotly.express as px
import pandas as pd
import numpy as np
from datetime import timedelta
import glob
import datetime as dt

import xarray as xr
import os 
from PIL import Image
import time
import random

my_dict={}

for i in np.arange(1,17,1):
    n=random.randint(1,10)
    m=random.randint(1,10)
    data=[[pd.Timestamp('2020-10-06'),n,m],[pd.Timestamp('2020-10-07'),m,n],[pd.Timestamp('2020-10-08'),n,m],[pd.Timestamp('2020-10-09'),m,n]]
    my_dict[i]=pd.DataFrame(data=data, columns=['time', 'Temp','Precip'])

final_dict={'day1':{'model1':{'weight1':{'crop1':my_dict[1], 'cropA':my_dict[2]},
                           'weight2':{'crop2':my_dict[3], 'cropB':my_dict[4]}},
                 
                 'model2':{'weight3':{'crop3':my_dict[5], 'cropC':my_dict[6]},
                           'weight4':{'crop4':my_dict[7], 'cropD':my_dict[8]}}},
         
         'day2':{'model3':{'weight5':{'crop5':my_dict[9], 'cropE':my_dict[10]},
                           'weight6':{'crop6':my_dict[11], 'cropF':my_dict[12]}},
                
                 'model4':{'weight7':{'crop7':my_dict[13], 'cropG':my_dict[14]},
                           'weight8':{'crop8':my_dict[15], 'cropH':my_dict[16]}}}}

app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])


# Helpers #####################################################################

def get_dict_keys(varlist):
    dic = final_dict
    for var in varlist:
        dic = dic.get(var)
    return dic

def make_options(option_iter):
    return [{'label': i, 'value': i} for i in option_iter]

class InputContainer:
    def __init__(self, input_dd='day1', model_dd='model1', weight_dd='weight1',
        crop_dd='crop1', columns_dd='Precip'):
        
        self.container = [ 
            dbc.FormGroup(
                [dbc.Label("Init Date"),
                    dcc.Dropdown(
                        id='init_dd',
                        options= make_options(final_dict.keys()),
                        value=input_dd,
                        clearable=False,
                    ),
                ]
            ),
            dbc.FormGroup(
                [dbc.Label("Model"),
                    dcc.Dropdown(
                        id='model_dd',
                        value = model_dd,
                        clearable=False,
                    ),
                ]
            ), 
            dbc.FormGroup(
                [dbc.Label("Weighting"),
                    dcc.Dropdown(
                        id='weight_dd',
                        value = weight_dd,
                        clearable=False,
                    ),
                ]
            ),
            dbc.FormGroup(
                [dbc.Label("Crop"),
                    dcc.Dropdown(
                        id='crop_dd',
                        value = crop_dd,
                        clearable=False,
                    ),
                ]
            ),          
            dbc.FormGroup(
                [dbc.Label("Forecast Variable"),
                    dcc.Dropdown(
                        id='columns_dd',
                        value = columns_dd,
                        options = make_options(['time', 'Temp','Precip']),
                        clearable=False,
                    ),
                ]
            ),
        ]
        self.assign_index()

    def assign_index(self): # unused: just for an idea 
        self.idx = {} 
        for i, item in enumerate(self.container): 
            if hasattr(item.children[1], 'id'): # skip the label at 0
                self.idx[item.children[1].id] = i

    def update_options(self, varname, options, selected=0):  # unused: just for an idea 
        pos = self.idx[varname]
        print(self.container[pos].children[1])
        if hasattr(self.container[pos].children[1],'id'):
            setattr(self.container[pos].children[1],'options', options)
            setattr(self.container[pos].children[1],'value', options[selected].get('value'))


# UI #####################################################################
controls = dbc.Card(
    html.Div(
        id='dynamic-input-container',
        children = InputContainer().container),
    body=True,
)


app.layout = dbc.Container(
    [
        html.Hr(),
        dbc.Row([
            dbc.Col([
                dbc.Row([
                    dbc.Col(controls)
                ],  align="start"), 
            ],xs = 2)
            ,
            dbc.Col([
                dbc.Row(html.Div(id='selected_data')),
                dbc.Row([
                    dbc.Col(dcc.Graph(id="crop-graph")),
                ])                
            ])
        ],), 
    ], 
    fluid=True,
)
    
# Callbacks #####################################################################

#set the model
@app.callback(
    [Output('model_dd', 'options'),
     Output('model_dd', 'value')],
    [Input('init_dd', 'value')],
    )
def update_model_options(input_dd):
    print('update_model_options():')
    options = make_options(get_dict_keys([input_dd]).keys())
    return options, options[0].get('value')

#set the weight
@app.callback(
    [Output('weight_dd', 'options'),
    Output('weight_dd', 'value')],
    [Input('model_dd', 'value')],
    [State('init_dd', 'value')])
def update_weight_options(model_dd, input_dd):
    print('update_weight_options():')
    options = make_options(get_dict_keys([input_dd, model_dd]).keys())
    return options, options[0].get('value')

#set the crop
@app.callback(
    [Output('crop_dd', 'options'),
    Output('crop_dd', 'value')],
    [Input('weight_dd', 'value')],
    [State('init_dd', 'value'),
     State('model_dd', 'value')])
def update_crop_options(weight_dd, input_dd, model_dd):
    print('update_crop_options():')
    options = make_options(get_dict_keys([input_dd, model_dd, weight_dd]).keys())
    return options, options[0].get('value')

# #set the variable
# @app.callback(
#     [Output('columns_dd', 'options'),
#      Output('columns_dd','value')],
#     [Input('crop_dd', 'value')],
#     [State('init_dd', 'value'),
#      State('model_dd', 'value'),
#      State('weight_dd', 'value')])
# def set_column_options(crop_dd, input_dd, model_dd, weight_dd):
#     print('update_column_options():')
#     options = make_options(get_dict_keys([input_dd, model_dd, weight_dd, crop_dd]).columns[1:])
#     return options, options[0].get('value')

def make_plot(df, var):
    fig = go.Figure(
            data=[go.Scatter(x=df['time'], y=df[var], name=var)],
            layout={
                'yaxis': {'title': f'Plot of <b>{var}</b>'}
            }
        )
    return fig


no_data_fig = {"layout": {
        "xaxis": { "visible": False},
        "yaxis": {"visible": False},
        "annotations": [
            { "text": "",
                "xref": "paper",
                "yref": "paper",
                "showarrow": False,
                "font": {"size": 20 }
            }]
        }
    }

@app.callback(
    Output('crop-graph', 'figure'),
    [Input('init_dd', 'value'),
     Input('model_dd', 'value'),
     Input('weight_dd', 'value'),
     Input('crop_dd', 'value'),
     Input('columns_dd','value')]
    )
def plot_data(init_dd, model_dd, weight_dd, crop_dd, columns_dd):
    if crop_dd is None or weight_dd is None or model_dd is None or columns_dd is None: return None
    print('plot_data():', init_dd, model_dd, weight_dd, crop_dd, columns_dd)
    try:
        data = final_dict[init_dd][model_dd][weight_dd][crop_dd]
        data_col = data[columns_dd]
    except:
        return no_data_fig
    return make_plot(data, columns_dd)


app.run_server(mode='external', port = 8098, debug=True)   


Related