Plotly-Dash: Update a trace on a graph that doesn't have its own dropdown

1.7k views Asked by At

This question is basically an addendum to this previously asked question:

Properly setting up callbacks for dynamic dropdowns plotly dash

Now, I want to add a second trace to my plots which would be on a secondary y-axis. The data for the plot on the secondary y-axis would come from a similarly structured dict and dataframe, with similar naming conventions as well. Here is what I have.

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

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)

index3= [1,2,3,4]
columns3 =['time', '2m_temp_24hdelta_prod' , 'total_precip_24hdelta_prod']

index4= [1,2,3,4]
columns4 = ['time', '2m_temp_24hdelta_area', 'total_precip_24hdelta_area']

df_deltas_prod = {'corn': pd.DataFrame(index=index3, columns = columns3,
                                data= np.random.randn(len(index3),len(columns3))).cumsum(),
                'soybeans' : pd.DataFrame(index=index3, columns = columns3,
                                     data= np.random.randn(len(index3),len(columns3))).cumsum()}

df_deltas_area= {'corn': pd.DataFrame(index=index4, columns = columns4,
                                data= np.random.randn(len(index4),len(columns4))).cumsum(),
               'soybeans' : pd.DataFrame(index=index4, columns = columns4,
                                     data= np.random.randn(len(index4),len(columns4))).cumsum()}

# mimic data properties of your real world data
df_deltas_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_deltas_prod['corn'].set_index('time', inplace = True)
df_deltas_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_deltas_prod['soybeans'].set_index('time', inplace = True)

df_deltas_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_deltas_area['corn'].set_index('time', inplace = True)
df_deltas_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_deltas_area['soybeans'].set_index('time', inplace = True)

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


controls = dbc.Card(
    [    dbc.FormGroup(
            [
                dbc.Label("Crop"),
                dcc.Dropdown(
                    id='crop_dd',
                    options=[{'label': k.title(), 'value': k} for k in list(df_vals_prod.keys())],
                    value=list(df_vals_prod.keys())[0],
                    clearable=False,
                ),
            ]
        ),    
        
        
        
        dbc.FormGroup(
            [
                dbc.Label("Weighting"),
                dcc.Dropdown(
                    id='weight_dd',
                    options=[{'label': k, 'value': k} for k in all_options.keys()],
                    value='Area',
                    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"), 
                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,
)
    



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

# Weighting selection.
@app.callback( # Dataframe PROD or AREA
    Output('columns_dd', 'options'),
    # layout element: dcc.RadioItems(id='weight_dd'...)
    [Input('weight_dd', 'value')])
def set_columns_options(weight):
    varz =  [{'label': i, 'value': i} for i in all_options[weight]]
    return [{'label': i, 'value': i} for i in all_options[weight]]

# Columns selection
@app.callback( 
    Output('columns_dd', 'value'),
    [Input('columns_dd', 'options')])
def set_columns(available_options):
    return available_options[1]['value']

# Crop selection
@app.callback( 
    Output('crop_dd', 'value'),
    [Input('crop_dd', 'options')])
def set_crops(available_crops):
    return available_crops[0]['value']




# Make a figure based on the selections
@app.callback( # Columns 2m_temp_prod, or....
    Output('crop-graph', 'figure'),
    [Input('weight_dd', 'value'),
     Input('crop_dd', 'value'),
     Input('columns_dd', 'value')])
def make_graph(weight, available_crops, vals):
    
    # data source / weighting
    if weight == 'Production':
        dfv = df_vals_prod
        #dfd = df_deltas_prod
    if weight == 'Area':
        dfv = df_vals_area
        #dfd= df_deltas_area
    
    # plotly figure
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    if 'precip' in vals:
        fig.add_trace(go.Scatter(x=df_vals_prod[available_crops]['time'], y=round((dfv[available_crops][vals].cumsum()/25.4),2),
                                mode = 'lines', line=dict(color='lime', width=4),
                                hovertemplate='Date: %{x|%d %b %H%M} UTC<br>Precip: %{y:.2f} in<extra></extra>'), secondary_y=False)
    else:
        fig.add_trace(go.Scatter(x=df_vals_prod[available_crops]['time'], y=round(((dfv[available_crops][vals]-273.15)*(9/5))+32,2),
                                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.add_trace(go.Bar(x=dfd[available_crops].index, y=dfd[available_crops][deltas]), secondary_y=True)
    fig.update_layout(title=dict(text='Crop: ' + available_crops.title() + ', Weight: ' +weight+ ', Variable: '+ vals))
    fig.update_layout(yaxis2_showgrid=False,showlegend=False, 
                      width=1500,height=800,yaxis_zeroline=False, yaxis2_zeroline=False)
    fig.update_layout(template="plotly_dark", plot_bgcolor='#272B30', paper_bgcolor='#272B30')
    fig.update_yaxes(automargin=True)
    return(fig)

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

Notice how the dict names and the dataframe columns within the dicts have similar names. I would like those to stay together on the plot.

For example, the user selects Weighting: Production, Crop: Corn, Forecast Variable: 2m_temp_prod. This should plot a line plot. Now, I want to add a secondary y-axis, where 2m_temp_24hdelta_prod is plotted (comes from df_deltas_prod['corn']['2m_temp_24hdelta_prod']. Notice though that I don't want a dropdown for this, I just want it to be plotted based on the other dropdown selections. Finally, if the user switches to Weighting: Area, Crop: Corn, Forecast Variable: 2m_temp_area, the secondary y-axis would have plotted df_deltas_area['corn']['2m_temp_24hdelta_area']. Hope this is clear.

1

There are 1 answers

4
vestland On BEST ANSWER

Just to see if I'm understanding your logic correctly here, I'm going to build on the snippet in the answer to the linked question, and not the snippet you've provided here. If it turns out that the structure of the app produced by the snippet below is in fact what you're looking for, I'll see if I can work that into the code snippet in this question. (But first, please provide a fully working snippet with necessary imports).

The logic:

As you'll see by studying the snippet below, this suggestion is limited to showing a related column ('2m_temp_prod' versus '2m_temp_area') on the secondary y-axis from another dict, but for the same category like 'corn' or 'soybeans'. So if the primary axis shows '2m_temp_prod' for ''corn from 'df_vals_prod' on the primary axis, then the secondary yaxis shows '2m_temp_area' for 'corn' from 'df_vals_prod'.

To follow the described logic, a key addendum in the updated code snippet is:

# secondary yaxis
column_implied_lst = [e for e in dfd2[available_crops].columns if e[:4]==selected_column[:4]]
column_implied = column_implied_lst[0]

fig.add_trace(go.Bar(x=dfd2[available_crops].index,
                     y=dfd2[available_crops][column_implied],
                     marker_color = "rgba(255,0,0,0.4)"),
              secondary_y=True)
fig.update_layout(yaxis2=dict(title=dict(text='DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ column_implied)))

Edit: String matching

I've used a very rudimentary string-matching approach with column_implied_lst = [e for e in dfd2[available_crops].columns if e[:4]==selected_column[:4]]. In building a solution to your answer, I raised the question How to retrieve partial matches from a list of strings which turned out to get some very good answers. Take a look at those if my provided approach does not suit your needs.

The Dash app:

enter image description here

Complete code (first pitch)

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):
    #global selected_column
    
    # data source / weighting
    if selected_produce == 'prod':
        dfd = df_vals_prod
        dfd2 = df_vals_area
    if selected_produce == 'area':
        dfd = df_vals_area
        dfd2 = df_vals_prod
    
    # plotly figure
    
    # primary yaxis
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    fig.add_trace(go.Scatter(x=dfd[available_crops].index, y=dfd[available_crops][selected_column]), secondary_y=False)
    fig.update_layout(yaxis1=dict(title=dict(text='DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ selected_column)))
    
    # secondary yaxis
    
    column_implied_lst = [e for e in dfd2[available_crops].columns if e[:4]==selected_column[:4]]
    column_implied = column_implied_lst[0]
    
    fig.add_trace(go.Bar(x=dfd2[available_crops].index,
                         y=dfd2[available_crops][column_implied],
                         marker_color = "rgba(255,0,0,0.4)"),
                  secondary_y=True)
    fig.update_layout(yaxis2=dict(title=dict(text='DF: ' + selected_produce +' | Crops: ' + available_crops + ' | Column: '+ column_implied)))
    
    # layout makeover
    fig.update_layout(title=dict(text='Column to match: '+ selected_column + '| Implied match: ' +column_implied))
    fig['layout']['yaxis2']['showgrid'] = False
    
    return(fig)

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