1

I am working with two dictionaries, with each dictionary having 4 keys. The keys in both dictionaries are the same and correspond to a crop belt in the US.

df_vals.keys()
dict_keys(['corn', 'soybeans', 'winterwheat', 'springwheat'])

df_deltas.keys()
dict_keys(['corn', 'soybeans', 'winterwheat', 'springwheat'])

Within each key there is a dataframe with 5 columns, where the first column is time and the other columns are various variables. An example:

df_vals['corn'].head()
time    2m_temp_prod    2m_temp_area    total_precip_prod   total_precip_area
0   2020-09-17 00:00:00 299.346777  299.799234  0.000000    0.000000
1   2020-09-17 06:00:00 294.039512  294.443352  0.191070    0.286952
2   2020-09-17 12:00:00 292.959274  293.182931  0.155765    0.216606
3   2020-09-17 18:00:00 301.318046  301.767516  0.421768    0.485691
4   2020-09-18 00:00:00 300.623567  300.979650  0.363572    0.501164

df_deltas['corn'].head()
time    2m_temp_24hdelta_prod   2m_temp_24hdelta_area   total_precip_24hdelta_prod  total_precip_24hdelta_area
0   2020-09-17  -0.330566   -0.294223   -1.441738   -0.896948
1   2020-09-18  -0.063527   -0.066953   -3.242770   -2.002193
2   2020-09-19  -0.276225   -0.238248   -1.954929   -1.326568
3   2020-09-20  -0.778811   -0.747444   1.080549    0.523297
4   2020-09-21  -0.121823   -0.008793   -2.857210   -1.974432

Notice df_vals is 6-hourly and df_deltas is 24 hours. Now, I want to make a plot with a double y-axis, where a variable in df_vals is plotted as a line and the corresponding variable in df_deltas is plotted as a bar. This is easy to do and can be done, like so:

fig = make_subplots(specs=[[{"secondary_y": True}]])

time_vals=df_vals['corn']['time']
time_deltas=df_deltas['corn']['time']

temp_prod_vals=df_vals['corn']['2m_temp_prod']
temp_prod_deltas=df_deltas['corn']['2m_temp_24hdelta_prod']


fig.add_trace(go.Scatter(x=time_vals, y=((temp_prod_vals-273)*(9/5)+32),mode='lines', line=dict(color='red', width=4), yaxis='y1', hovertemplate='Date: %{x|%d %b %H%M} UTC<br>Temp: %{y:.2f} F<extra></extra>'),secondary_y=False)
fig.add_trace(go.Bar(x=time_deltas, y=round((temp_prod_deltas*(9/5)),2), marker_color='black', opacity=0.6, yaxis='y2', hovertemplate='Date: %{x|%d %b}<br>Delta: %{y:.2i} F<extra></extra>'),secondary_y=True)

That generates this plot:

enter image description here

Next, we want to add dropdown menus to switch between different crops (switch between the keys in our df_vals and df_deltas dicts). That can be done like so:

button1= [dict(method= 'update',
                args= [{'y': [((df_vals[i]['2m_temp_prod']-273)*(9/5)+32), round((df_deltas[i]['2m_temp_24hdelta_prod']*(9/5)),2)]}],
                label=i.title()) for i, j in list(zip(df_vals.keys(), df_deltas.keys()))]

Great, now we have a plot that goes through each dict key and updates depending on which crop you pick.

enter image description here

Now, what if we want to cycle through other columns (variables) in a particular dataframe? So far we have only been plotting temperatures. Let's plot precipitation now. We need to add a second button to do this like so:

button2= [dict(method= 'update',
                args= [{'y': [(df_vals[i]['total_precip_prod'].cumsum())/25.4, round((df_deltas[i]['total_precip_24hdelta_prod']/25.4),2)]}],
                label=i.title()) for i, j in list(zip(df_vals.keys(), df_deltas.keys()))]

This button now looks exactly like our first button, where each dropdown option is a key in our dicts. The completed plot looks like this: enter image description here

This plot works as intended, but is kind of clunky since we have two dropdowns with the same exact options. Now we have arrived at the purpose of this post. Instead of having two buttons for the variables we are plotting (temperature and precip), I want one button for the crop belt and one button for the variable. However, I can't seem to figure out how to orient my code to accomplish this. Any help would be appreciated.

Edit:

df_vals['corn']
time    2m_temp_prod    2m_temp_area    total_precip_prod   total_precip_area
0   2020-09-18 00:00:00 299.346777  299.799234  0.000000    0.000000
1   2020-09-18 06:00:00 294.039512  294.443352  0.191070    0.286952
2   2020-09-18 12:00:00 292.959274  293.182931  0.155765    0.216606
3   2020-09-18 18:00:00 301.318046  301.767516  0.421768    0.485691
4   2020-09-19 00:00:00 300.623567  300.979650  0.363572    0.501164
... ... ... ... ... ...
56  2020-10-02 00:00:00 301.177141  301.052273  0.371209    0.408515
57  2020-10-02 06:00:00 295.874298  295.720135  0.281793    0.300564
58  2020-10-02 12:00:00 293.838787  293.686738  0.586887    0.549365
59  2020-10-02 18:00:00 302.384474  302.191334  0.492712    0.493798
60  2020-10-03 00:00:00 300.920766  300.817993  0.522374    0.531138

df_deltas['corn']
time    2m_temp_24hdelta_prod   2m_temp_24hdelta_area   total_precip_24hdelta_prod  total_precip_24hdelta_area
0   2020-09-18  -0.330566   -0.294223   -1.441738   -0.896948
1   2020-09-19  -0.063527   -0.066953   -3.242770   -2.002193
2   2020-09-20  -0.276225   -0.238248   -1.954929   -1.326568
3   2020-09-21  -0.778811   -0.747444   1.080549    0.523297
4   2020-09-22  -0.121823   -0.008793   -2.857210   -1.974432
5   2020-09-23  0.484487    0.258438    -0.603610   -0.475656
6   2020-09-24  -0.779540   -0.865384   0.068153    0.094013
7   2020-09-25  -1.373656   -1.330636   -0.377604   -0.248026
8   2020-09-26  -0.792595   -0.778074   -0.218221   -0.161647
9   2020-09-27  -0.608407   -0.610188   0.143853    0.129186
10  2020-09-28  -0.653274   -0.660259   -0.096111   -0.025385
11  2020-09-29  -0.533418   -0.537711   -0.441704   -0.370940
12  2020-09-30  -0.321460   -0.348938   -0.992614   -0.762149
13  2020-10-01  0.005672    -0.129977   -0.266382   -0.219744
14  2020-10-02  0.294875    0.080317    -0.043508   -0.063877
15  2020-10-03  0.000000    0.000000    0.000000    0.000000

The other 3 keys (soybeans, winterwheat, and springwheat) all have the same structure as this, just different values.

Edit 3: Posting the full code and libraries.

import xarray as xr
import glob
import datetime as dt
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

I load in several different netcdf time series files using xarray. These files are forecast model data which run out 15 days from the day the model was run. There are also delta files, which tell you the change of the forecast variable from 24 hours ago (so they only go out 14 days).

corn=glob.glob('/Users/eli.turaskyriskpulse.com/Documents/Python_data/plotly_practice/20200812_00/20200812_00_ec_ens_*'+'corn'+'_timeseries.nc')
soybean=glob.glob('/Users/eli.turaskyriskpulse.com/Documents/Python_data/plotly_practice/20200812_00/20200812_00_ec_ens_*'+'soybeans'+'_timeseries.nc')
winterwheat=glob.glob('/Users/eli.turaskyriskpulse.com/Documents/Python_data/plotly_practice/20200812_00/20200812_00_ec_ens_*'+'winterwheat'+'_timeseries.nc')
springwheat=glob.glob('/Users/eli.turaskyriskpulse.com/Documents/Python_data/plotly_practice/20200812_00/20200812_00_ec_ens_*'+'springwheat'+'_timeseries.nc')
all_files=[corn, soybean,winterwheat,springwheat]

crop_names=['corn', 'soybeans', 'winterwheat', 'springwheat']
data={}
for i in all_files:
    data[str(i)]=xr.open_mfdataset(i).load()

This loads in all of the files into a dictionary, with each key representing the crop for which the variables are forecasted for. Each time series is set up in 6 hourly format. However, I want to further adjust this because I need the time for the deltas to be in 24 hour increments, instead of 6 hour.

today=dt.date.today()
df_vals={}
df_deltas={}
for i in data.keys():
    df_vals[str(i)]=data[i].to_dataframe().reset_index()
    df_vals[i]['time']=pd.date_range((today-dt.timedelta(days=5)), (today+dt.timedelta(days=10)), freq='6H')
    
    df_deltas[i]=df_vals[i].filter(regex='delta')
    df_deltas[i]['time']=pd.date_range((today-dt.timedelta(days=5)), (today+dt.timedelta(days=10)), freq='6H')
    df_deltas[i]=df_deltas[i].set_index('time')
    
    cols = [c for c in df_deltas[i].columns if "precip" in c or "temp" in c]
    aggs = {c:'sum' if 'precip' in c else 'mean' for c in cols}
    df_deltas[i]=df_deltas[i].resample('24h').agg(aggs)
    
    df_deltas[i]=df_deltas[i].reset_index().fillna(0)
    
    df_vals[i]=df_vals[i][df_vals[i].columns.drop(list(df_vals[i].filter(regex='delta')))].fillna(value=0)

for i in df_vals.keys():
    df_vals[i]=df_vals[i].drop(['snowfall_prod', 'snowfall_area'], 1)

Now I have reached what I wanted. Two dictionaries that contain 4 keys (representing the 4 crops), with df_vals at 6-hourly time resolution and df_deltas at 24-hourly time resolution. Then, I plot.

fig = make_subplots(specs=[[{"secondary_y": True}]])

time_vals=df_vals['corn']['time']
time_deltas=df_deltas['corn']['time']

temp_prod_vals=df_vals['corn']['2m_temp_prod']
temp_prod_deltas=df_deltas['corn']['2m_temp_24hdelta_prod']


fig.add_trace(go.Scatter(x=time_vals, y=((temp_prod_vals-273)*(9/5)+32),mode='lines', line=dict(color='red', width=4), yaxis='y1', hovertemplate='Date: %{x|%d %b %H%M} UTC<br>Temp: %{y:.2f} F<extra></extra>'),secondary_y=False)
fig.add_trace(go.Bar(x=time_deltas, y=round((temp_prod_deltas*(9/5)),2), marker_color='black', opacity=0.6, yaxis='y2', hovertemplate='Date: %{x|%d %b}<br>Delta: %{y:.2i} F<extra></extra>'),secondary_y=True)

button1= [dict(method= 'update',
                args= [{'y': [((df_vals[i]['2m_temp_prod']-273)*(9/5)+32), round((df_deltas[i]['2m_temp_24hdelta_prod']*(9/5)),2)]}],
                label=i.title()) for i, j in list(zip(df_vals.keys(), df_deltas.keys()))]
button2= [dict(method= 'update',
                args= [{'y': [(df_vals[i]['total_precip_prod'].cumsum())/25.4, round((df_deltas[i]['total_precip_24hdelta_prod']/25.4),2)]}],
                label=i.title()) for i, j in list(zip(df_vals.keys(), df_deltas.keys()))]

fig.update_layout(yaxis2_showgrid=False, title_text='Plot title',showlegend=False,
                  title_x=0.4,
                  width=850,
                  height=450,annotations=
                  [dict(text='Temps:', x=0.02, xref='paper', y=1.12, yref='paper', align='left', showarrow=False),
                  dict(text='Precip:', x=0.36, xref='paper', y=1.12, yref='paper', align='left', showarrow=False)],
                  updatemenus=[dict(active=0,
                                   x=0.1, y=1.18, 
                                   pad={"r": 10, "t": 10},
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=button1),
                               dict(active=0,
                                   x=0.40, y=1.18, 
                                   pad={"r": 10, "t": 10},
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=button2)
                              ]) 
Eli Turasky
  • 981
  • 2
  • 11
  • 28
  • 1
    It seems to me that here you should use dash and in particular [Pattern-Matching Callbacks](https://medium.com/plotly/pattern-matching-callbacks-in-dash-9014eee99858) – rpanai Sep 22 '20 at 19:09
  • Is this not possible in the python version? I was hoping to avoid learning Dash or JS to accomplish this task. – Eli Turasky Sep 22 '20 at 19:14
  • 2
    @EliTurasky You *do not* have to learn JS. But you should learn a bit about Dash. Plotly, Dash and Python is a fantastic combination! – vestland Sep 23 '20 at 06:23
  • 1
    @vestland Of course I should. Haha. I think I am going to actually take the time to learn about Dash, since it seems like it should be able to accomplish this task and other similar ones I have in mind. Thanks for the suggestions – Eli Turasky Sep 23 '20 at 10:11
  • 2
    @EliTurasky If you take the time to share your datasets as describede [here](https://stackoverflow.com/questions/63163251/pandas-how-to-easily-share-a-sample-dataframe-using-df-to-dict/63163254#63163254), I'll take a closer look when I find the time. – vestland Sep 23 '20 at 10:53
  • @vestland I will edit my post to share the data in a few hours or so. Thanks for the help. – Eli Turasky Sep 23 '20 at 11:03
  • @vestland I have updated to provide the full data. Let me know if you need anything else and thank you again. – Eli Turasky Sep 23 '20 at 11:48
  • @EliTurasky Did you read the link I provided? The intention is to have *one runnable* code snippet to reproduce your figure. – vestland Sep 23 '20 at 12:40
  • @vestland Yes, I did. I figured I could provide you one entry in the two dataframes and then you could make sample data to fit the rest. If you want me to provide all 8 dataframes here though, I can. I think that posting the full dataframe looks the neatest and gives the most information. – Eli Turasky Sep 23 '20 at 13:18
  • 1
    @EliTurasky I see. Well, I might do that. Eventually. But keep in mind that every minute anyone spends on reproducing your issue is a minute less spent answering your question. – vestland Sep 23 '20 at 13:51
  • @EliTurasky Sone other users often provide answers much quicker when it comes to Dash though =) – vestland Sep 23 '20 at 13:53
  • 1
    @vestland Understood. I am going to start trying to learn Dash today. It isn't any rush, just take a look when you have time. – Eli Turasky Sep 23 '20 at 14:00
  • @EliTurasky reading through the comments it seems like one road block is getting missed... as it stands now it would be cumbersome to get started on this question. In your "Edit2" can you also include code to generate the data and the libraries you are using? – jayveesea Sep 24 '20 at 11:32
  • @jayveesea Just updated the post. It is getting very long at this point. Any suggestions on how I could shorten it all up now that I have tried to add and explain everything? – Eli Turasky Sep 24 '20 at 12:04

0 Answers0