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.
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:
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:
Complete code (first pitch)