Combining Multiple Subplots with Drop Down Menu Buttons

I have monthly sales information on various companies (Amount, Profit) and would like to display all of this information in a single interactive Plotly chart - I.e. for each unique company, there should be a bar chart displaying sales amount per month as well as a line chart displaying profit per month, the two charts will share the x axis for time but have separate y axes (see example below).

This is simple enough to do for a single company using subplots, but I would like to be able to switch between companies using a dropdown menu. I have been able to get soething working, but am running into various bugs that I cannot get around.

Code to Reproduce Data:


import pandas as pd
import numpy as np
import itertools
from datetime import datetime

np.random.seed(2021)

company_list = ['Company_A', 'Company_B', 'Company_C', 'Company_D', 'Company_E']
datelist = pd.date_range(start="2020-01-01", end='2021-01-01', freq='MS').to_list()

df = pd.DataFrame(list(itertools.product(company_list, datelist)))
df.columns = ['Company', 'Date']

df['Amount'] = np.random.choice(range(0,10000), df.shape[0])
df['Profit'] = np.random.choice(range(0,10000), df.shape[0])


df.head()

enter image description here

I have a function which takes a data frame (assuming the same format as the one just created above) and creates a Plotly chart which has the two plots (Amount and Profit) for each company, and has a drop down menu to change from company to company.

Function to Make Multiple Plots and Dropdown Menu:

from plotly import graph_objs as go
from plotly.subplots import make_subplots

def make_multi_plot(df):
    
    fig = make_subplots(rows=2, cols=1,
                    shared_xaxes=True,
                    vertical_spacing=0.02)

    for customer in list(df.Company.unique()):
        
        trace1 = go.Bar(
            x=df.loc[df.Company.isin([customer])].Date, 
            y=df.loc[df.Company.isin([customer])].Amount, 
            name = "Amount - " + str(customer))

        trace2 = go.Scatter(
                x=df.loc[df.Company.isin([customer])].Date,
                y=df.loc[df.Company.isin([customer])].Profit,
                name = "Profit - " + str(customer)
            )

        fig.append_trace(trace1,1,1)
        fig.append_trace(trace2,2,1)


    def create_layout_button(customer):
        return dict(label = customer,
                    method = 'restyle',
                    args = [{'visible': [cust == customer for cust in list(df.Company.unique())],
                             'title': customer,
                             'showlegend': True}])

    fig.update_layout(
        updatemenus=[go.layout.Updatemenu(
            active = 0,
            buttons = [create_layout_button(customer) for customer in list(df.Company.unique())]
            )
        ])
    
    fig.show()

At first glance, this seems to be doing what I want. However, I am running into 2 issues which I can’t solve:

  1. When the function is first called, it plots the data for ALL of the companies on the two plots, rather than just the first company (which is what I want). This does fix itself once you do select a company from the dropdown menu, although that introduces us to our next issue…Output when you first call the make_multi_plot function

  2. When you do select a Company from the dropdown menu, it doesn’t actually update the plots correctly, it is using the wrong company’s data to make the plots. If you look at the legend for the two plots, you can see that it is actually plotting the data for different companies in the set. I have no idea why this is happening and haven’t been able to find any real pattern in how it’s confusing the various plots with the buttons. When you choose a company from the dropdown, it doesn't update the plots with the right data

Appreciate any and all help!

@demianeg

Your fig.data has the length equal to Ld = 2*number of companies.
Hence to avoid displaying initially all Ld traces, you must set the visibility of traces not only in the button definitions,
but also in the definition of each trace.

That’s why I inserted the following lines of code:

Ld=len(fig.data)
for k in range(2, Ld):
    fig.update_traces(visible=False, selector = k)

that have as effect, setting visible=False, for all traces except the two corresponding to the Company A.

With your code at the selection of a Company you don’t get displayed the pair Amount, Profit for that company, because your code
doesn’t set visibility for all 2*Lc traces, where Lc is the number of unique companies, but only for Lc traces.

A working code is this one:

def make_multi_plot(df):
    
    fig = make_subplots(rows=2, cols=1,
                    shared_xaxes=True,
                    vertical_spacing=0.02)

    for customer in df.Company.unique():
        
        trace1 = go.Bar(
            x=df.loc[df.Company.isin([customer])].Date, 
            y=df.loc[df.Company.isin([customer])].Amount, 
            name = "Amount - " + str(customer))
        fig.append_trace(trace1,1,1)
        trace2 = go.Scatter(
                x=df.loc[df.Company.isin([customer])].Date,
                y=df.loc[df.Company.isin([customer])].Profit,
                name = "Profit - " + str(customer))
        fig.append_trace(trace2,2,1)

    Ld=len(fig.data)
    Lc =len(df.Company.unique())
    for k in range(2, Ld):
        fig.update_traces(visible=False, selector = k)
    def create_layout_button(k, customer):
        
        visibility= [False]*2*Lc
        for tr in [2*k, 2*k+1]:
            visibility[tr] =True
        return dict(label = customer,
                    method = 'restyle',
                    args = [{'visible': visibility,
                             'title': customer,
                             'showlegend': True}])    
    

    fig.update_layout(
        updatemenus=[go.layout.Updatemenu(
            active = 0,
            buttons = [create_layout_button(k, customer) for k, customer in enumerate(df.Company.unique())]
            )
        ])
    
    fig.show()
    
2 Likes

@empet thanks for the help and explanation, that makes a ton of sense and has helped me scale this to include more than just two traces. Thank you!

Hi @empet ,

I have a similar problem. I just tried your solution too. But this time, I have three traces. Two graphs are in the same plot. Third one is a table. Here is my code:

def make_multi_plot(df1, df2, item_list):
    
    date_time = str(datetime.datetime.now())[0:16].replace(':','-').replace(' ','_')
    
    with open('Model_Raporu_'+date_time+'.html', 'a') as f:
        
        fig = make_subplots(rows=2, 
                        cols=1,
                        shared_xaxes=True,
                        vertical_spacing=0.05,
                        specs = [[{}], [{"type": "table"}]]
                       )

        for item_id in item_list:
            
            trace1 = go.Scatter(
                x=df1.loc[df1.ITEM_ID.isin([item_id])].SALES_DATE, 
                y=df1.loc[df1.ITEM_ID.isin([item_id])].y,
                mode='lines+markers',
                name = "orjinal - " + str(item_id))
            fig.append_trace(trace1,1,1)
            
            trace1x = go.Scatter(
                x=df1.loc[df1.ITEM_ID.isin([item_id])].SALES_DATE, 
                y=df1.loc[df1.ITEM_ID.isin([item_id])].y_hat,
                mode='lines+markers',
                name = "tahmin - " + str(item_id))
            fig.append_trace(trace1x,1,1)
        
            trace2 = go.Table(    
                header = dict(
                values = df2[df2.ITEM_ID.isin([item_id])].columns.tolist(),
                font = dict(size=10),
                align = "left"),
                cells = dict(
                values = [df2[df2.ITEM_ID.isin([item_id])][k].tolist() for k in df2[df2.ITEM_ID.isin([item_id])].columns[:]],
                align = "left"
                )
            )
            fig.append_trace(trace2,2,1)
        
        Ld = len(fig.data)
        Lc = len(item_list)
        for k in range(3, Ld):
            fig.update_traces(visible=False, selector = k)
    
        def create_layout_button(k, item_id):
            
            visibility= [False]*3*Lc
            for tr in [3*k, 3*k+1]:
                visibility[tr] =True
            return dict(label = item_id,
                        method = 'restyle',
                        args = [{'visible': visibility,
                                 'title': item_id,
                                 'showlegend': True}])    
    

        fig.update_layout(
           updatemenus=[go.layout.Updatemenu(
           active = 0,
           buttons = [create_layout_button(k, item_id) for k, item_id in enumerate(item_list)],
           x = 0.5,
           y = 1.15
           )
        ],
           title = 'Model Raporu',
           template = 'plotly_dark',
           height=800
       )
    
        fig.show()
        f.write(fig.to_html(full_html=False, include_plotlyjs='cdn'))

When change the dropdown button, only the graph above changes. But table remains same. I could not change it by dropdown. Here is the image:

How can I change the table with dropdown?

You made an error here. This should be a range and the k+1 should be between brackets.

Replace by:

 visibility = [False] * 3 * Lc
            for tr in range(3 * k, 3 * (k + 1)):
                visibility[tr] = True