Plotly python : how to make a dropdown menu with several lines?

Question:

I would like to make a dropdown menu in a plotly line graph such as the one in this post but with several lines.
Let’s take a sample dataframe :

import pandas as pd
df = pd.DataFrame({"Date": ["2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03"],
                   "Animal" :["Cat","Cat","Cat","Cat","Cat","Cat","Dog","Dog","Dog","Dog","Dog","Dog"],
                   "Category":["Small","Small","Small","Big","Big","Big","Small","Small","Small","Big","Big","Big"],
                   "Quantity":[2,4,3,5,1,2,6,5,6,4,2,1]})

df["Date"] = df["Date"].astype('datetime64')

I would like to make a plotly line graph with Date in x axis, Quantity in y axis, a curve for each Animal and a filter for each Category. I tried the following function but the result is not good as the lines are shaking. Would you know please where my mistake is ?

import plotly.graph_objects as go
def plot_line_go_graph(df,col_x,col_y,col_color = None,col_filter = None,add_points = False) :
    
    df_graph = df.copy()
        
    if add_points :
        param_mode='lines+markers'
        param_name='lines+markers'
    else :
        param_mode='lines'
        param_name='lines'
    
    fig = go.Figure()
    
    if col_filter is None :
    
        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name))
        else :
            for c in df_graph[col_color].unique() :
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_color]==c][col_x], y=df_graph[df_graph[col_color]==c][col_y],mode=param_mode,name=c))
                
    else :
        
        df_graph[col_filter] = df_graph[col_filter].fillna("NaN")
        
        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name,visible = True))
        else :
            for c in df_graph[col_color].unique() :
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_color]==c][col_x], y=df_graph[df_graph[col_color]==c][col_y],mode=param_mode,name=c,visible = True))
        
        updatemenu = []
        buttons = []

        # button with one option for each dataframe
        buttons.append(dict(method='restyle',
                                label="All",
                                visible=True,
                                args=[{'y':[df_graph[col_y]],
                                       'x':[df_graph[col_x]],
                                       'type':'scatter'}, [0]],
                                )
                          )
        for f in df_graph[col_filter].unique():
            buttons.append(dict(method='restyle',
                                label=f,
                                visible=True,
                                args=[{'y':[df_graph[df_graph[col_filter]==f][col_y]],
                                       'x':[df_graph[df_graph[col_filter]==f][col_x]],
                                       'type':'scatter'}, [0]],
                                )
                          )

        # some adjustments to the updatemenus
        updatemenu = []
        your_menu = dict()
        updatemenu.append(your_menu)

        updatemenu[0]['buttons'] = buttons
        updatemenu[0]['direction'] = 'down'
        updatemenu[0]['showactive'] = True

        # add dropdown menus to the figure
        fig.update_layout(updatemenus=updatemenu)
        
        if col_color is None :
            fig.update_layout(showlegend=False)
    
    fig.update_layout({
            'plot_bgcolor': 'rgba(0,0,0,0)',
            'paper_bgcolor': 'rgba(0,0,0,0)',
        },
        hoverlabel=dict(
            #bgcolor="white", 
            font_size=12, 
            #font_family="Rockwell"
        ),
        hovermode = "x"
    )

    fig.update_xaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    fig.update_yaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    
    fig.show()
plot_line_go_graph(df,"Date","Quantity",col_color = "Animal", col_filter = "Category",add_points = False)

Wrong output

Asked By: Ewdlam

||

Answers:

I think there’s a few issues here. The first problem is that when you plot the data filtered by "Category" (such as "Big" or "Small"), you’ll be plotting data that repeats dates and this is what causes the lines to go backwards in time which makes the plot hard to interpret.

You can instead add traces by performing a groupby on the col_color, col_filter – in your example, if you groupby ["Animal", "Category"], you should have four unique groups and therefore four traces.

Then I think you can redesign the buttons to toggle the visibility of each trace based on whether the name of the trace contains the particular col_filter. For example, if your traces are called ("Cat","Big"),("Cat,"Small"),("Dog","Big"),("Dog","Small"), when you click on the button "Big" it will toggle the visibility of the four traces as [True, False, True, False]. And the "All" button will set the visibility of all traces to be True.

import pandas as pd
import plotly.graph_objects as go

df = pd.DataFrame({"Date": ["2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03"],
                   "Animal" :["Cat","Cat","Cat","Cat","Cat","Cat","Dog","Dog","Dog","Dog","Dog","Dog"],
                   "Category":["Small","Small","Small","Big","Big","Big","Small","Small","Small","Big","Big","Big"],
                   "Quantity":[2,4,3,5,1,2,6,5,6,4,2,1]})

df["Date"] = df["Date"].astype('datetime64')

def plot_line_go_graph(df,col_x,col_y,col_color = None,col_filter = None,add_points = False) :
    
    df_graph = df.copy()
        
    if add_points :
        param_mode='lines+markers'
        param_name='lines+markers'
    else :
        param_mode='lines'
        param_name='lines'
    
    fig = go.Figure()
    
    if col_filter is None :
    
        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name))
        else :
            for c in df_graph[col_color].unique() :
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_color]==c][col_x], y=df_graph[df_graph[col_color]==c][col_y],mode=param_mode,name=c))
                
    else :
        
        df_graph[col_filter] = df_graph[col_filter].fillna("NaN")

        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name,visible = True))
        else :
            for group, df_group in df_graph.groupby([col_color, col_filter]):
                fig.add_trace(go.Scatter(
                    x=df_group[col_x], 
                    y=df_group[col_y],
                    mode=param_mode,
                    name=f"{group}",
                    visible=True
                ))
    
        updatemenu = []
        buttons = []

        # button with one option for each dataframe
        buttons.append(dict(method='restyle',
                                label="All",
                                visible=True,
                                args=[{'visible' : [True]*len(fig.data)}]
                                )
                          )
    
        for group, df_filter in df_graph.groupby([col_filter]):
            visible_traces = [group in trace['name'] for trace in fig.data]
            buttons.append(dict(method='restyle',
                                label=group,
                                visible=True,
                                args=[{'visible' : visible_traces}]
                            ))

        # some adjustments to the updatemenus
        updatemenu = []
        your_menu = dict()
        updatemenu.append(your_menu)

        updatemenu[0]['buttons'] = buttons
        updatemenu[0]['direction'] = 'down'
        updatemenu[0]['showactive'] = True

        # add dropdown menus to the figure
        fig.update_layout(updatemenus=updatemenu)
        
        if col_color is None :
            fig.update_layout(showlegend=False)
    
    fig.update_layout({
            'plot_bgcolor': 'rgba(0,0,0,0)',
            'paper_bgcolor': 'rgba(0,0,0,0)',
        },
        hoverlabel=dict(
            #bgcolor="white", 
            font_size=12, 
            #font_family="Rockwell"
        ),
        hovermode = "x"
    )

    fig.update_xaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    fig.update_yaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    
    return fig

fig = plot_line_go_graph(df,"Date","Quantity",col_color = "Animal", col_filter = "Category",add_points = False)
fig.show()

enter image description here

Update: based on the discussion in the comment thread below, the traces need to be redesigned a bit. There still has to be one trace for each animal + category combination – there is no good way around this because if you don’t plot each trace individually, then you cannot control them individually. For example, if you have a trace for Dog (big+small) and Cat (big+small), then how do you select small only?

But what we can do is have all four traces, but make them behave like two traces using a legend group and only showing each legend group once – any traces for a particular animal get their own entry in the legend and are selected and deselected together. The tricky part is that when you click the button for "Big" or "Small", this button needs to know which traces are "Big" or "Small" so we can put the information about the category into customdata, and then retrieve it to determine which traces should be visible when you click on "Big" or "Small" from the dropdown.

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

df = pd.DataFrame({"Date": ["2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03","2022-10-01","2022-10-02","2022-10-03"],
                   "Animal" :["Cat","Cat","Cat","Cat","Cat","Cat","Dog","Dog","Dog","Dog","Dog","Dog"],
                   "Category":["Small","Small","Small","Big","Big","Big","Small","Small","Small","Big","Big","Big"],
                   "Quantity":[2,4,3,5,1,2,6,5,6,4,2,1]})

df["Date"] = df["Date"].astype('datetime64')

def plot_line_go_graph(df,col_x,col_y,col_color = None,col_filter = None,add_points = False) :
    
    df_graph = df.copy()
        
    if add_points :
        param_mode='lines+markers'
        param_name='lines+markers'
    else :
        param_mode='lines'
        param_name='lines'
    
    fig = go.Figure()
    
    if col_filter is None :
    
        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name))
        else :
            for c in df_graph[col_color].unique() :
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_color]==c][col_x], y=df_graph[df_graph[col_color]==c][col_y],mode=param_mode,name=c))
                
    else :
        plotly_colors = px.colors.qualitative.Plotly
        color_map_length = len(df_graph[col_color].unique())
        color_map = {name:color for (name,color )in zip(df_graph[col_color].unique(),plotly_colors[:color_map_length])}
        df_graph[col_filter] = df_graph[col_filter].fillna("NaN")
        

        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name,visible = True))
        else :

            ## the traces have no information regarding the col_filter
            ## so we put this information about col_color and col_filter
            ## (in this case the information about animal and category)
            ## into the customdata as an f-string
            for color, df_color in df_graph.groupby(col_color):
                color_count = 0
                for filter, df_filter in df_color.groupby(col_filter):
                    if color_count == 0:
                        showlegend=True
                        color_count += 1
                    else:
                        showlegend=False
                    fig.add_trace(go.Scatter(
                        x=df_filter[col_x], 
                        y=df_filter[col_y],
                        marker=dict(color=color_map[color]),
                        mode=param_mode,
                        name=f"{color}",
                        customdata=[f"{color} + {filter}"],
                        legendgroup=color,
                        showlegend=showlegend,
                        visible=True
                    ))
    
        updatemenu = []
        buttons = []

        # button with one option for each dataframe
        buttons.append(dict(method='restyle',
                                label="All",
                                visible=True,
                                args=[{'visible' : [True]*len(fig.data)}]
                                )
                          )

        for group, df_filter in df_graph.groupby([col_filter]):
            visible_traces = [group in trace['customdata'][0] for trace in fig.data]
            buttons.append(dict(method='restyle',
                                label=group,
                                visible=True,
                                args=[{'visible' : visible_traces}]
                            ))

        # some adjustments to the updatemenus
        updatemenu = []
        your_menu = dict()
        updatemenu.append(your_menu)

        updatemenu[0]['buttons'] = buttons
        updatemenu[0]['direction'] = 'down'
        updatemenu[0]['showactive'] = True

        # add dropdown menus to the figure
        fig.update_layout(updatemenus=updatemenu)
        
        if col_color is None :
            fig.update_layout(showlegend=False)
    
    fig.update_layout({
            'plot_bgcolor': 'rgba(0,0,0,0)',
            'paper_bgcolor': 'rgba(0,0,0,0)',
        },
        hoverlabel=dict(
            #bgcolor="white", 
            font_size=12, 
            #font_family="Rockwell"
        ),
        hovermode = "x"
    )

    fig.update_xaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    fig.update_yaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    
    return fig

fig = plot_line_go_graph(df,"Date","Quantity",col_color = "Animal", col_filter = "Category",add_points = False)
fig.show()

enter image description here

Answered By: Derek O

Thanks to the great help of @DerekO, I founded a solution for the expected outuput :

import plotly.graph_objects as go
def plot_line_go_graph(df,col_x,col_y,col_color = None,col_filter = None,add_points = False) :
    
    df_graph = df.copy()
        
    if add_points :
        param_mode='lines+markers'
        param_name='lines+markers'
    else :
        param_mode='lines'
        param_name='lines'
    
    fig = go.Figure()
    
    if col_filter is None :
    
        if col_color is None :
            fig.add_trace(go.Scatter(x=df_graph[col_x], y=df_graph[col_y],mode=param_mode,name=param_name))
        else :
            for c in df_graph[col_color].unique() :
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_color]==c][col_x], y=df_graph[df_graph[col_color]==c][col_y],mode=param_mode,name=c))
                
    else :
        
        df_graph[col_filter] = df_graph[col_filter].fillna("NaN")

        if col_color is None :
            L_filter = []
            for f in df_graph[col_filter].unique():
                fig.add_trace(go.Scatter(x=df_graph[df_graph[col_filter]==f][col_x], y=df_graph[df_graph[col_filter]==f][col_y],mode=param_mode,name=param_name,visible = False))           
                L_filter.append(f)
            df_graph_gb = df_graph.groupby([col_x],as_index=False).agg({col_y:"sum"})
            fig.add_trace(go.Scatter(x=df_graph_gb[col_x], y=df_graph_gb[col_y],mode=param_mode,name=param_name,visible = True))
            L_filter.append("Global")
        else :
            L_filter = []
            for group, df_group in df_graph.groupby([col_color, col_filter]):
                fig.add_trace(go.Scatter(
                    x=df_group[col_x], 
                    y=df_group[col_y],
                    mode=param_mode,
                    name=group[0],
                    visible=False
                ))
                L_filter.append(group[1])
                
            df_graph_gb = df_graph.groupby([col_x,col_color],as_index=False).agg({col_y:"sum"})
            for clr in df_graph_gb[col_color].unique() :
                fig.add_trace(go.Scatter(
                        x=df_graph_gb[df_graph_gb[col_color]==clr][col_x], 
                        y=df_graph_gb[df_graph_gb[col_color]==clr][col_y],
                        mode=param_mode,
                        name=clr,
                        visible=True
                    ))
                L_filter.append("Global")
                
    
        updatemenu = []
        buttons = []
        
        for b in ["Global"] + list(df_graph[col_filter].unique()) :
            visible_traces = [True if b == i else False for i in L_filter]
            buttons.append(dict(method='restyle',
                                label=b,
                                visible=True,
                                args=[{'visible' : visible_traces}]
                            ))

        # some adjustments to the updatemenus
        updatemenu = []
        your_menu = dict()
        updatemenu.append(your_menu)

        updatemenu[0]['buttons'] = buttons
        updatemenu[0]['direction'] = 'down'
        updatemenu[0]['showactive'] = True

        # add dropdown menus to the figure
        fig.update_layout(updatemenus=updatemenu)
        
        if col_color is None :
            fig.update_layout(showlegend=False)
    
    fig.update_layout({
            'plot_bgcolor': 'rgba(0,0,0,0)',
            'paper_bgcolor': 'rgba(0,0,0,0)',
        },
        hoverlabel=dict(
            #bgcolor="white", 
            font_size=12, 
            #font_family="Rockwell"
        ),
        hovermode = "x"
    )

    fig.update_xaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    fig.update_yaxes(showspikes=True, spikecolor = 'black', showline=True, linewidth=1,linecolor='black', ticks = "outside", tickwidth = 1, tickcolor = 'black',ticklen = 5)
    
    return fig

fig = plot_line_go_graph(df,"Date","Quantity",col_color = "Animal", col_filter = "Category",add_points = False)
fig.show()

enter image description here

Answered By: Ewdlam