防止链式回调将下拉列表切换为原始值破折号

Preventing chained callbacks from switching dropdown to original value dash

我有一段很长的代码,用于几个源自多重嵌套字典的链式回调。我有我想提供的所有必要的下拉菜单和选项。但是,每当我将此示例中的 'crop' 下拉菜单更改为原始选项(玉米)以外的其他内容时,它都会重置下面的 'Weighting' 下拉菜单。同样,如果我更改 'Weighting' 下拉菜单,它会将 'Forecast Variable' 下拉菜单重置为原始选项。我怎样才能防止这种情况发生?链接回调的要点是改变一个选项会改变绘制的数据,因为它们都是链接的。

我觉得这里的数据不重要?但它的功能是这样的:

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

上面的精确字典将输出一个数据帧。数据框中的列将是最终将被绘制的 'forecast variable' 。如果我确实需要添加数据,我可以尝试这样做,但是字典非常大。

这是我目前的代码。请注意,该图是空的,因为我还没有走那么远。

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)   

编辑:添加示例虚拟数据。请注意,当更改某些选项组合时,其他选项如何切换回原始值。想防止这种情况发生。

具体的数据示例有所帮助。我看到了

  • 数据集存储在嵌套字典中
  • 您希望允许用户 select 特定数据集(每个 user-input 选项取决于嵌套结构中的 previous/upstream select 离子)。
  • 因为这里的嵌套结构意味着对于给定的输入更改,您想要更新输入选项仅针对 subsequent/downstream 个输入

关于更好地控制链式回调的问题,我认为这是在正确位置使用 Input()State() 的问题。

试试这个(我重命名了你的 final_dict 以便更容易监控正在发生的事情):

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)   

这是另一个版本。

  • 我注意到您希望数据列 column_dd 保持固定而不更新(可能假设不同版本的最终数据集中的列相同)。所以,我注释掉了更新 column_dd.
  • 的回调
  • 您还可以将输出合并为一个列表。
  • 我试图展示一种使用动态输入生成的方法,但最终不需要动态更新它(我无法指定相同的 output-id 两次,这很不方便。我只是暂时保留它demo,你不需要切换到这种风格。)请注意,它仍然可以在回调中用作 State() 并覆盖其属性)。
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)