Properly setting up callbacks for dynamic dropdowns plotly dash

6.4k views Asked by At

I am trying to create a Dash dashboard where dropdown options in one box are dependent on the previous dropdown selection.

The data consists of two dictionaries, with two keys each. Each key contains a dataframe with a couple of columns. The exact data:

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
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
from pandas import Timestamp
import numpy as np

df_vals_prod={'corn':pd.DataFrame({'time': {1: Timestamp('2020-09-23 06:00:00'),
  2: Timestamp('2020-09-23 12:00:00'),
  3: Timestamp('2020-09-23 18:00:00'),
  4: Timestamp('2020-09-24 00:00:00')},
 '2m_temp_prod': {1: 0.020584322444347606,
  2: 0.08973907730395358,
  3: 2.3866310395722463,
  4: 3.065472457668321},
 'total_precip_prod': {1: 1.372708470272411,
  2: 2.135683294556938,
  3: 1.9811172016307312,
  4: 2.1082116841869323}}),
'soybeans':pd.DataFrame({'time': {1: Timestamp('2020-09-23 06:00:00'),
  2: Timestamp('2020-09-23 12:00:00'),
  3: Timestamp('2020-09-23 18:00:00'),
  4: Timestamp('2020-09-24 00:00:00')},
 '2m_temp_prod': {1: 0.6989001827317545,
  2: -0.8699121426411993,
  3: -0.9484359259520706,
  4: 0.7391299158393124},
 'total_precip_prod': {1: -0.07639291299336869,
  2: 0.19182892415959496,
  3: 0.8719339093510236,
  4: 0.90586956349059}})}

df_vals_area={'corn':pd.DataFrame({'time': {1: Timestamp('2020-09-23 06:00:00'),
  2: Timestamp('2020-09-23 12:00:00'),
  3: Timestamp('2020-09-23 18:00:00'),
  4: Timestamp('2020-09-24 00:00:00')},
 '2m_temp_area': {1: -1.6820417878457192,
  2: -0.2856437053872421,
  3: 0.3864022581278122,
  4: 0.5873739667356371},
 'total_precip_area': {1: 1.3703311242708185,
  2: 0.25528434511264525,
  3: 0.5007488191835624,
  4: -0.16292114222272375}}),
'soybeans':pd.DataFrame({'time': {1: Timestamp('2020-09-23 06:00:00'),
  2: Timestamp('2020-09-23 12:00:00'),
  3: Timestamp('2020-09-23 18:00:00'),
  4: Timestamp('2020-09-24 00:00:00')},
 '2m_temp_area': {1: 1.3789989862086967,
  2: -0.7797086923820608,
  3: 1.0695635889750523,
  4: 1.136561500804678},
 'total_precip_area': {1: -0.6035111830104833,
  2: -0.18237330469451313,
  3: -0.7820158376898607,
  4: -0.6117188028872137}})}

app = JupyterDash(external_stylesheets=[dbc.themes.SLATE])
weight_opts=['Production','Area']

controls = dbc.Card(
    [    dbc.FormGroup(
            [
                dbc.Label("Crop"),
                dcc.Dropdown(
                    id="Crop",
                    options=[
                        {"label": col, "value": col} for col in list(df_vals_prod.keys())
                    ],
                    value=list(df_vals_prod.keys())[0],
                    clearable=False,
                ),
            ]
        ),    
        
        
        
        dbc.FormGroup(
            [
                dbc.Label("Weighting"),
                dcc.Dropdown(
                    id="weights",
                    options=[
                        {"label": i, "value": i} for i in weight_opts
                    ],
                    value=weight_opts[0],
                    clearable=False,
                ),
            ]
        ),
        dbc.FormGroup(
            [
                dbc.Label("Forecast Variable"),
                dcc.Dropdown(
                    id="forecast_v",
                ),
            ]
        ),

    ],
    body=True,
)

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

@app.callback(
    Output('forecast_v','options'),
    [Input('weights', 'value')]
)

def update_var_dropdown(weight):
    if weight=='Production':
        return [{'label': i, 'value': i} for i in df_vals_prod['corn'].columns[1:]]
    elif weight=='Area':
        return [{'label': i, 'value': i} for i in df_vals_area['corn'].columns[1:]]


@app.callback(
    Output("crop-graph", "figure"),
    [   Input("Crop", "value"),
        Input("weights", "value"),
        Input("forecast_v", "value"),

    ],
)

def crop_graph(Crop, val, weight):

    # plotly figure setup
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    
    if weight:
        fig.add_trace(go.Scatter(name=val, x=df_vals_prod[Crop]['time'], y=((df_vals_prod[Crop][val]-273)*(9/5))+32, mode = 'lines', line=dict(color='red', width=4),
                                hovertemplate='Date: %{x|%d %b %H%M} UTC<br>Temp: %{y:.2f} F<extra></extra>'), secondary_y=False,
                  )
        fig.update_yaxes(title_text="<b>Temp (F)<b>", color='red', secondary_y=False,)
        fig.update_yaxes(title_text="<b>24hr Forecast Change (F)</b>", secondary_y=True)

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

As you can see, this 6 hourly data and is meant to be plotted as a time series. Now I want to add a couple of dropdowns. The first dropdown (Crop) selects which crop to choose (corn or soybeans), which are the two keys from each dictionary.

The second dropdown (Weighting) now selects which dataframe we want to use. What the user selects in this second dropdown will determine the options to select in the third dropdown.

The third dropdown will select the actual variable (Forecast Variable), which is one of two columns available in each dataframe. So if in dropdown 2, "Production" is selected, the options for dropdown 3 would consist of '2m_temp_prod' or 'total_precip_prod'. For "Area" in dropdown 2, dropdown 3 options would be '2m_temp_area' or 'total_precip_area'.

Here is the code I have so far. I am able to setup the callback properly for the dropdowns, but I don't think my second callback is working properly. I understand how to create the dynamic dropdown, but I am not sure how to translate that to actually plotting the data.

That produces this plot. Notice that the dropdowns are what I want, but it doesn't plot. How do I add in the 'weight' to make the desired plot? What I would expect is just a simple line graph where the data depends on all of the dropdowns chosen.

enter image description here

Edit: as suggested by vestland, I am including a much smaller data sample. The specific values in this case don't matter, just the structure of the data. See above for more concise data.

1

There are 1 answers

9
vestland On BEST ANSWER

I haven't been able to figure out why your code fails. But I've been putting together an example that I think will come close to what you're looking for here. It builds on an example from the plotly docs, and so has a bit different layout from what you've got in your question. The main take-away is that three sets of radio buttons will let you:

  1. select a weight: ['prod', 'area'],
  2. which in turn will define the options in another callback: ['2m_temp_prod', 'total_precip_prod'] or ['2m_temp_area', 'total_precip_area'].
  3. And you're also able to select produce ['corn', 'soybeans']

It's very possible that I've misunderstood the logic of what you want to achieve here. But just give me some feedback along the way and we can work out the details.

Dash app for selection DF: prod | Crops: corn | Column: 2m_temp_prod

enter image description here

Dash app for selection DF: area | Crops: soybeans | Column: total_precip_area

enter image description here

Complete code:

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

# data
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
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
from pandas import Timestamp
import numpy as np

# data ##########################################################################
index1= [1,2,3,4]
columns1 =['time', '2m_temp_prod' , 'total_precip_prod']

index2= [1,2,3,4]
columns2 = ['time', '2m_temp_area', 'total_precip_area']

df_vals_prod = {'corn': pd.DataFrame(index=index1, columns = columns1,
                                data= np.random.randn(len(index1),len(columns1))).cumsum(),
                'soybeans' : pd.DataFrame(index=index1, columns = columns1,
                                     data= np.random.randn(len(index1),len(columns1))).cumsum()}

df_vals_area= {'corn': pd.DataFrame(index=index2, columns = columns2,
                                data= np.random.randn(len(index2),len(columns2))).cumsum(),
               'soybeans' : pd.DataFrame(index=index2, columns = columns2,
                                     data= np.random.randn(len(index2),len(columns2))).cumsum()}

# mimic data properties of your real world data
df_vals_prod['corn']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'), 
                                  Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_prod['corn'].set_index('time', inplace = True)
df_vals_prod['soybeans']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                      Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_prod['soybeans'].set_index('time', inplace = True)

df_vals_area['corn']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                  Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_area['corn'].set_index('time', inplace = True)
df_vals_area['soybeans']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                      Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_area['soybeans'].set_index('time', inplace = True)

# dash ##########################################################################
app = JupyterDash(__name__)

# weighting
all_options = {
    'prod': list(df_vals_prod[list(df_vals_prod.keys())[0]].columns),
    'area': list(df_vals_area[list(df_vals_prod.keys())[0]].columns)
}

app.layout = html.Div([
    dcc.RadioItems(
        id='produce-radio',
        options=[{'label': k, 'value': k} for k in all_options.keys()],
        value='prod'
    ),

    html.Hr(),
    
    dcc.RadioItems(
        id='crop-radio',
        options=[{'label': k, 'value': k} for k in list(df_vals_prod.keys())],
        value=list(df_vals_prod.keys())[0]
    ),

    html.Hr(),

    dcc.RadioItems(id='columns-radio'),

    html.Hr(),

    html.Div(id='display-selected-values'),
    
    dcc.Graph(id="crop-graph")
])

# Callbacks #####################################################################

# Weighting selection.
@app.callback( # Dataframe PROD or AREA
    Output('columns-radio', 'options'),
    # layout element: dcc.RadioItems(id='produce-radio'...)
    [Input('produce-radio', 'value')])
def set_columns_options(selected_produce):
    varz =  [{'label': i, 'value': i} for i in all_options[selected_produce]]
    print('cb1 output: ')
    print(varz)
    return [{'label': i, 'value': i} for i in all_options[selected_produce]]

# Columns selection
@app.callback( 
    Output('columns-radio', 'value'),
    # layout element: dcc.RadioItems(id='columns-radio'...)
    [Input('columns-radio', 'options')])
def set_columns(available_options):
    return available_options[0]['value']

# Crop selection
@app.callback( 
    Output('crop-radio', 'value'),
    # layout element: dcc.RadioItems(id='columns-radio'...)
    [Input('crop-radio', 'options')])
def set_crops(available_crops):
    return available_crops[0]['value']

# Display selections in its own div
@app.callback( # Columns 2m_temp_prod, or....
    Output('display-selected-values', 'children'),
    [Input('produce-radio', 'value'),
     Input('crop-radio', 'value'),
     Input('columns-radio', 'value')])
def set_display_children(selected_produce, available_crops, selected_column):
    return('DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ selected_column)

# Make a figure based on the selections
@app.callback( # Columns 2m_temp_prod, or....
    Output('crop-graph', 'figure'),
    [Input('produce-radio', 'value'),
     Input('crop-radio', 'value'),
     Input('columns-radio', 'value')])
def make_graph(selected_produce, available_crops, selected_column):
    
    # data source / weighting
    if selected_produce == 'prod':
        dfd = df_vals_prod
    if selected_produce == 'area':
        dfd = df_vals_area
    
    # plotly figure
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=dfd[available_crops].index, y=dfd[available_crops][selected_column]))
    fig.update_layout(title=dict(text='DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ selected_column))
    return(fig)

app.run_server(mode='inline', port = 8077, dev_tools_ui=True,
          dev_tools_hot_reload =True, threaded=True)

Edit 1 - Dropdown menus.

All you have to do to get the desired dropdown buttons is to change each

dcc.RadioItems()

to

 dcc.Dropdown()

Now you'll get:

enter image description here

Complete code:

from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

# data
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
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
from pandas import Timestamp
import numpy as np

# data ##########################################################################
index1= [1,2,3,4]
columns1 =['time', '2m_temp_prod' , 'total_precip_prod']

index2= [1,2,3,4]
columns2 = ['time', '2m_temp_area', 'total_precip_area']

df_vals_prod = {'corn': pd.DataFrame(index=index1, columns = columns1,
                                data= np.random.randn(len(index1),len(columns1))).cumsum(),
                'soybeans' : pd.DataFrame(index=index1, columns = columns1,
                                     data= np.random.randn(len(index1),len(columns1))).cumsum()}

df_vals_area= {'corn': pd.DataFrame(index=index2, columns = columns2,
                                data= np.random.randn(len(index2),len(columns2))).cumsum(),
               'soybeans' : pd.DataFrame(index=index2, columns = columns2,
                                     data= np.random.randn(len(index2),len(columns2))).cumsum()}

# mimic data properties of your real world data
df_vals_prod['corn']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'), 
                                  Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_prod['corn'].set_index('time', inplace = True)
df_vals_prod['soybeans']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                      Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_prod['soybeans'].set_index('time', inplace = True)

df_vals_area['corn']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                  Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_area['corn'].set_index('time', inplace = True)
df_vals_area['soybeans']['time'] =   [Timestamp('2020-09-23 06:00:00'), Timestamp('2020-09-23 12:00:00'),
                                      Timestamp('2020-09-23 18:00:00'), Timestamp('2020-09-24 00:00:00')]
df_vals_area['soybeans'].set_index('time', inplace = True)

# dash ##########################################################################
app = JupyterDash(__name__)

# weighting
all_options = {
    'prod': list(df_vals_prod[list(df_vals_prod.keys())[0]].columns),
    'area': list(df_vals_area[list(df_vals_prod.keys())[0]].columns)
}

app.layout = html.Div([
    dcc.Dropdown(
        id='produce-radio',
        options=[{'label': k, 'value': k} for k in all_options.keys()],
        value='area'
    ),
#     dcc.Dropdown(
#     id='produce-radio',
#     options=[
#         {'label': k, 'value': k} for k in all_options.keys()
#     ],
#     value='prod',
#     clearable=False),
    

    html.Hr(),
    
    dcc.Dropdown(
        id='crop-radio',
        options=[{'label': k, 'value': k} for k in list(df_vals_prod.keys())],
        value=list(df_vals_prod.keys())[0]
    ),

    html.Hr(),

    dcc.Dropdown(id='columns-radio'),

    html.Hr(),

    html.Div(id='display-selected-values'),
    
    dcc.Graph(id="crop-graph")
])

# Callbacks #####################################################################

# Weighting selection.
@app.callback( # Dataframe PROD or AREA
    Output('columns-radio', 'options'),
    # layout element: dcc.RadioItems(id='produce-radio'...)
    [Input('produce-radio', 'value')])
def set_columns_options(selected_produce):
    varz =  [{'label': i, 'value': i} for i in all_options[selected_produce]]
    print('cb1 output: ')
    print(varz)
    return [{'label': i, 'value': i} for i in all_options[selected_produce]]

# Columns selection
@app.callback( 
    Output('columns-radio', 'value'),
    # layout element: dcc.RadioItems(id='columns-radio'...)
    [Input('columns-radio', 'options')])
def set_columns(available_options):
    return available_options[0]['value']

# Crop selection
@app.callback( 
    Output('crop-radio', 'value'),
    # layout element: dcc.RadioItems(id='columns-radio'...)
    [Input('crop-radio', 'options')])
def set_crops(available_crops):
    return available_crops[0]['value']

# Display selections in its own div
@app.callback( # Columns 2m_temp_prod, or....
    Output('display-selected-values', 'children'),
    [Input('produce-radio', 'value'),
     Input('crop-radio', 'value'),
     Input('columns-radio', 'value')])
def set_display_children(selected_produce, available_crops, selected_column):
    return('DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ selected_column)

# Make a figure based on the selections
@app.callback( # Columns 2m_temp_prod, or....
    Output('crop-graph', 'figure'),
    [Input('produce-radio', 'value'),
     Input('crop-radio', 'value'),
     Input('columns-radio', 'value')])
def make_graph(selected_produce, available_crops, selected_column):
    
    # data source / weighting
    if selected_produce == 'prod':
        dfd = df_vals_prod
    if selected_produce == 'area':
        dfd = df_vals_area
    
    # plotly figure
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=dfd[available_crops].index, y=dfd[available_crops][selected_column]))
    fig.update_layout(title=dict(text='DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ selected_column))
    return(fig)

app.run_server(mode='inline', port = 8077, dev_tools_ui=True,
          dev_tools_hot_reload =True, threaded=True)