Black Lives Matter. Please consider donating to Black Girls Code today.

Creating dropdown with multiple options in loop

I am trying to plot a dictionary with one plot, but multiple dropdown options. Here is a sample of one of the four keys in the dictionary.

time	2m_temp_prod
0	2020-08-14 00:00:00	299.346777
1	2020-08-14 06:00:00	294.039512
2	2020-08-14 12:00:00	292.959274
3	2020-08-14 18:00:00	301.318046
4	2020-08-15 00:00:00	300.623567

Now, I try and plot this dictionary with 4 different options for the dropdowns.

for i in df_vals.keys():
    hi=go.Scatter(x=df_vals[i]['time'], y=((df_vals[i]['2m_temp_prod']-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>')

    updatemenus=[dict(
                        buttons=list([
                            dict(
                                method='update',
                                label=i,
                                args= [{'y': [df_vals[i]['2m_temp_prod']]}],
                            ),
                        ]),
                        direction="down",
                        pad={"r": 10, "t": 10},
                        showactive=True,
                        x=0.07,
                        xanchor="left",
                        y=1.15,
                        yanchor="top"
                        )]
                    
    
                                 

    layout = go.Layout(title='Sfc Temp and 24hr Forecast Change for ',updatemenus=updatemenus, yaxis2=dict(overlaying='y', side='right'), annotations=[dict(text='Ag belt:', x=0, xref='paper', y=1.10, yref='paper', align='left', showarrow=False)])
    fig=go.Figure(hi, layout=layout)
    fig.show()

However, this plots all 4 plots separately, each one having only one dropdown option. How do I set this up to only make one plot and 4 dropdown options?

Here is a picture for reference.

@LOVESN

To get the plot as you stated, you have to define only a trace,
and 4 buttons:

import pandas as pd
import numpy as np
import plotly.graph_objects as go
from datetime import datetime

df = pd.DataFrame({'time': [datetime(2020, 8, 1, 8+j, 5*k, 0)  for j in range(3)  for k in range(12)],
                   'A': np.random.randint(23, 51, 36),
                   'B': np.random.randint(30, 65, 36),
                   'C': np.random.randint(27, 63, 36),
                   'D': np.random.randint(40, 78, 36)})

fig = go.Figure(go.Scatter(x=df['time'], y=df['A'], mode='lines', line_color='RoyalBlue'))

cols = df.columns[1:]
linecolors = ['RoyalBlue', 'red', 'green', 'magenta']

my_buttons = [dict(method = "restyle",
                   args = [{'y': [ df[c] ], 
                        "line.color": linecolors[k]}    ],
                   label = c) for k, c in enumerate(cols)]

fig.update_layout(width=800, height=400,
                 updatemenus=[dict(active=0,
                                   x= 1, y=1, 
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=my_buttons)
                              ]) 

Your initial Figure is updated by each button. The plotly.js function that performs such an update is restyle, notupdate (see their role, here: https://plotly.com/python/dropdowns/)

1 Like

Nice. This is exactly what I was looking for. Thanks!

@empet How would I modify this solution to include a second y-axis? I want to add a go.bar() trace on the same graph as the lines, but with a different y-axis. I am struggling to adapt this code to perform that task. Something like this image:

@LOVESN

How did you plot bars over x= df[‘time’] ?

Without data I can only suggest how to redefine the buttons:

import pandas as pd
import numpy as np
import plotly.graph_objects as go
from  plotly.subplots import make_subplots

from datetime import datetime


df = pd.DataFrame({'xdata': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                   'A': np.random.randint(23, 51, 10),
                   'B': np.random.randint(30, 65, 10),
                   'C': np.random.randint(27, 63, 10),
                   'D': np.random.randint(40, 78, 10)})


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

fig.add_trace(go.Scatter(x=df['xdata'], y=df['A'], mode='lines', line_color='RoyalBlue'), secondary_y=False)

y_bar = np.random.randint(3, 10, 5)
fig.add_trace(go.Bar(x= [2, 4, 6, 8, 10], y= y_bar, width=0.5,  marker_color='#E9967A'), secondary_y=True)

cols = df.columns[1:]
linecolors = ['RoyalBlue', 'red', 'green', 'magenta']
barcolors = ['#E9967A', '#8FBC8F', '#483D8B','#2F4F4F']


y_barupdate = [y_bar, np.random.randint(3, 8, 5), np.random.randint(2, 10, 5), np.random.randint(4, 11, 5)]
my_buttons = [dict(method = "restyle",
                
                   args = [{'y': [df[c],   y_barupdate[k]],
                           'line.color': linecolors[k],
                           'marker.color': barcolors[k]}, [0,1]],
                   label = c) for k, c in enumerate(cols)]

fig.update_layout(width=800, height=400,
                 updatemenus=[dict(active=0,
                                   x= 1, y=1, 
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=my_buttons)
                              ]) 

To understand why button args are defined like that, please read a tutorial on restyle method and references therein: https://chart-studio.plotly.com/~empet/15607

Here is the data from the dataframe.

time	2m_temp_24hdelta_prod
0	2020-08-15	-0.330566
1	2020-08-16	-0.063527
2	2020-08-17	-0.276225
3	2020-08-18	-0.778811
4	2020-08-19	-0.121823
5	2020-08-20	0.484487
6	2020-08-21	-0.779540
7	2020-08-22	-1.373656
8	2020-08-23	-0.792595
9	2020-08-24	-0.608407
10	2020-08-25	-0.653274
11	2020-08-26	-0.533418
12	2020-08-27	-0.321460
13	2020-08-28	0.005672
14	2020-08-29	0.294875
15	2020-08-30	0.000000

Here is the code used to generate the actual plot.

trace1=go.Scatter(x=df_vals['corn']['time'], y=((df_vals['corn']['2m_temp_prod']-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>')
trace2=go.Bar(x=df_deltas['corn']['time'], y=round((df_deltas['corn']['2m_temp_24hdelta_prod']*(9/5)),2), marker_color='black', opacity=0.6, yaxis='y2', hovertemplate='Date: %{x|%d %b}<br>Delta: %{y:.2i} F<extra></extra>')

data1=[trace1, trace2]

updatemenus=[{'buttons': [{'method': 'update',
                                 'label': 'corn',
                                 'args': [{'y': [data1]},]
                                  }]}]

layout = go.Layout(updatemenus=updatemenus, yaxis2=dict(overlaying='y', side='right'))
go.Figure(data=data, layout=layout)

Hope this helps.

@LOVESN

I think I gave you all details to succeed yourself in defining the new Figure version.

1 Like

How would I update the title of the plot to reflect the column name each time a new dropdown is selected? Basically the dropdown and title name would be the same.

@LOVESN

To update the plot title, you should first asssign a title to layout.title.text, and then to perform a title update in each button arg. In this case since you are updating both data and layout the restyle method, must be replaced by update:

import pandas as pd
import numpy as np
import plotly.graph_objects as go
from  plotly.subplots import make_subplots

df = pd.DataFrame({'xdata': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                   'A': np.random.randint(23, 51, 10),
                   'B': np.random.randint(30, 65, 10),
                   'C': np.random.randint(27, 63, 10),
                   'D': np.random.randint(40, 78, 10)})


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

fig.add_trace(go.Scatter(x=df['xdata'], y=df['A'], mode='lines', line_color='RoyalBlue'), secondary_y=False)

y_bar = np.random.randint(3, 10, 5)
fig.add_trace(go.Bar(x= [2, 4, 6, 8, 10], y= y_bar, width=0.5,  marker_color='#E9967A'), secondary_y=True)

cols = df.columns[1:]
linecolors = ['RoyalBlue', 'red', 'green', 'magenta']
barcolors = ['#E9967A', '#8FBC8F', '#483D8B','#2F4F4F']


y_barupdate = [y_bar, np.random.randint(3, 8, 5), np.random.randint(2, 10, 5), np.random.randint(4, 11, 5)]
my_buttons = [dict(method = "update",
                
                   args = [{'y': [df[c],   y_barupdate[k]],
                           'line.color': linecolors[k],
                           'marker.color': barcolors[k]}, 
                           {'title.text': f'Column: {c}'}, [0,1]],
                   label = c) for k, c in enumerate(cols)]

fig.update_layout(title_text = f'Column: {cols[0]}',
                  title_x =0.5,
                  width=800, height=400,
                  updatemenus=[dict(active=0,
                                   x= -0.2, y=1, 
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=my_buttons)
                              ]) 

@empet When I add in the yaxis.range calls the plot does not update when selecting a new dropdown button. What might be causing this?

my_buttons = [dict(method = "update",
                
                   args = [{'y': [df[c],   y_barupdate[k]],
                           'line.color': linecolors[k],
                           'marker.color': barcolors[k],
                           'yaxis.range': [np.min(df[c]),np.max(df[c])],
                           'yaxis2.range': [np.min(y_barupdate[k]), np.max(y_barupdate[k])]}, 
                           {'title.text': f'Column: {c}'}, [0,1]],
                   label = c) for k, c in enumerate(cols)]

@LOVESN

The updates for layout must be inserted in a new dict, not in the same with data,
i.e.

my_buttons = [dict(method = "update",
                
                   args = [{'y': [df[c],   y_barupdate[k]],
                           'line.color': linecolors[k],
                           'marker.color': barcolors[k]},
                          { 'yaxis.range': [np.min(df[c]),np.max(df[c])],
                           'yaxis2.range': [np.min(y_barupdate[k]), np.max(y_barupdate[k])], 
                           'title.text': f'Column: {c}'}, [0,1]],
                      label = c) for k, c in enumerate(cols)]

I gave you a few links. If you are reading the notebook destinated to update method https://chart-studio.plotly.com/~empet/15605/update-method-called-within-an-update/#/ you can see the following rule:

The args to be passed to the update method consist in two dicts and a list.
The first dict defines updates for data and data attributes,
the second dict updates layout attributes, while the list is a list of trace indices (indices of elements in the list fig.data, that are updated within the first dict).

args = [{'y': [y0_new, y_2_new],
         'marker_color': ['green', 'magenta'}, #dict for data updates
        {'yaxis.type': 'log'}, #dict for layout updates
        [0, 2] #list of trace indices
1 Like

Thanks for all your help with this code @empet . I now have (what I assume) a more difficult task of adding a second data source. For now, the plots I have constructed are all for temperatures. Now I want to add in precipitation data. The data works the same, i.e., there is a line plot and a second y-axis with bars. The times are also the same. How would I go about incorporating this into a new button. My current button has 4 options, denoting 4 different crop belts. I want the new button to have 2 options, one denoted Temperature and the other Precipitation. Thus, I could click “Corn” and “Precipitation”, or “Corn” and “Temperature” and the data would change accordingly.

 my_buttons = list([[dict(method = "update",
                       args = [{'y': [((df_temps_prod[c]-273)*(9/5)+32), round((df_temps_deltas_prod[c]*(9/5)),2)]},
                       {'title.text': 'Temp and 24-hour Temp Delta for: '+c.title(),
                       'yaxis.range': [myround(np.min(((df_temps_prod[c]-273)*(9/5)+32)-10)), myround(np.max(((df_temps_prod[c]-273)*(9/5)+32)+10))]},[0,1]],
                       label = c.title()) for k,c in enumerate(cols)],
                   dict(method = 'update',
                       args = [{'y': [(df_precip_prod[c]/25.4).cumsum(), round(df_precip_deltas_prod[c]/25.4,2)]},[0,1]],
                       label = c.title()) for k,c in enumerate(cols)])

I tried doing something like this, but it doesn’t seem to work. For reference, the column names are the same in both dataframes.

@LOVESN

Let us say you have already defined, buttonA, buttonB, buttonC, buttonD. For precipitation data you’ll define more 4 buttons: button1, button2, button3, button4. In this case the definition of updatemenus contains two dicts: one for the dropdown menu to update temeprature data, and the second one for precipitation data:

updatemenus=[dict(
                                   active=0,
                                   x= -0.2, y=1, 
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=[buttonA, buttonB, buttonC, buttonD]),
                              
                               dict(
                                   active=0,
                                   x= -0.2, y=0.75,  #The position of this second dropdown menu is assigned by trial and error
                                   xanchor='left', 
                                   yanchor='top',
                                   buttons=[button1, button2, button3, button4])
                              ])  

@empet This helps me understand how the buttons work but isn’t exactly what I am looking for. I want a dropdown with 4 different options, and the second dropdown to only have 2 options. Here are two images for what I am trying to do.

The data changes dynamically based on the selection. For example, I can click “corn” and “total_precip” or “corn” and “2m_temps”. Does that make sense? I want to do something similar with my dropdowns. When I click “corn” and then click “temperature”, I want my dropdown to display that specific data. However, now I want to display precipitation data (but still for corn), so I change menu 2 to "precipitation. Is that possible to do?

Hi @empet. Have you been able to take a look at the post above? Hope you can help as I am still stuck. Thanks!

Hi @LOVESN

I can look at your plots, but without data and code I cannot express any opinion. It’s difficult to understand what’s going on from a story about temperature and precipitation.

Let me get back to you. Trying something new here. If I cannot get it to work, I will reach out once more.

Okay, I am back to the same point I was before. Here is the situation:

I have a plot that has two buttons, each button having 4 options. The 4 options represent a crop, in this case corn, soybeans, winter wheat and spring wheat. The first button represents temperature and the second button represents precipitation. The plot looks like so:

However, this is not what I want. I still want to have 2 buttons, but I want the data to be different and can’t figure out how to make it work. In this case, I want button 1 to contain 4 options, with the options being the crops listed above. Then, I want the second button to have 2 options, being temperature and precipitation.

As a result, I could select ‘corn’ and ‘temperature’ and it would plot. Then, say I want to keep the first option the same, ‘corn’, but I want to look at ‘precipitation’ now instead. How can I create code that will dynamically switch these data? I essentially want to clean up the dropdowns to make them more clear. Is my question clear? @empet

Edit: I can share code for my data sources if need be.

So I made a little schematic to try and help inform what I am trying to do.

Each crop has two sets of data associated with it. One is temperature and the other is precipitation. The name of the crop is just an identifier for which temperature or precipitation dataset to plot. Hopefully this adds some value to what is happening. @empet

@LOVESN

I haven’t understood yet, because it’s not clear how is defined your fig.data, i.e. how many traces is contains.

Eventually take a look at this answer: Advanced Dropdown Menus with Plotly if you want to selectively reset or ignore certain properties when restyling. Besides 'undefined' there is the option 'null'. `null resets a property to the default one, whileundefined`` applies no change to the current state.

*In the above answer replace “two each” by “To each” :slight_smile: