Reputation: 1061
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.
Upvotes: 1
Views: 1956
Reputation: 61074
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).
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)))
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.
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)
Upvotes: 1