Odd behavior with double dropdowns and subplots

Hi there,

I’ve been attempting to make a non-Dash interactive HTML plot with dual dropdowns, one for data filtering and another for variable selection. I’ve used this example as a basic template and tweaked it to fit the format I want. This seems to work as intended with just one plot displayed at a time, but when I incorporate a second table subplot I start getting unexpected results and the wrong traces are displayed. I’ve included code for a toy example reproduction below.

For 2 subplots with N traces each, and only one pair of traces visible at a time, it seems as though my list of visible traces should be 2*N long and for a given variable at index i, the subplots set to be visible would be at index 2*i and 2*i+1. This does work with the initialized data. But after changing data with the ‘group’ dropdown, the traces are displayed out of order.

Through experimentation I found I can somehow get the right traces to show up after group selection if I use a visible list of length of only N with variable number i set to True, although the legend labels and initialized traces aren’t ordered correctly in this case, and it doesn’t make sense to me why this would work. Furthermore, this only works if I have an odd number of variables to plot. if the number is even, it breaks.

So my current workaround is: make sure to use an odd number of variables, initialize the charts with blank data to force a group selection, use a visibility parameter list in my restyle dropdown of length N, and hide the legend. But all of this seems very hack-y and I’m wondering if I’m missing something. If anyone can give me some guidance, I’d appreciate it - please let me know if I can explain anything better.

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

df1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A'], 'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03'], 'var1': [1, 1, 1], 'var2': [2, 2, 2], 'var3': [3, 3, 3]})
df2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B'], 'Submission Date': ['2023-01-04', '2023-01-05', '2023-01-06'], 'var1': [11, 11, 11], 'var2': [12, 12, 12], 'var3': [13, 13, 13]})
df = pd.concat([df1, df2])
df['Submission Date'] = pd.to_datetime(df['Submission Date'])

# dummy extra variable for testing - behavior changes when the number of variables is odd vs even(??)
#df['extravar'] = [4, 4, 4, 14, 14, 14]

# Data for the tables
tabdf1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A'], 'Submission Date': ['2023-01-01', '2023-02-11', '2023-03-05'], 'variable': ['var1', 'var2', 'var3'], 'value': ['A var1 val', 'A var2 val', 'A var3 val']})
tabdf2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B'], 'Submission Date': ['2022-06-01', '2022-07-11', '2022-08-01'], 'variable': ['var1', 'var2', 'var3'], 'value': ['B var1 val', 'B var2 val', 'B var3 val']})
table_df = pd.concat([tabdf1, tabdf2])


# fns for building data update dropdown. x is the same every time so doesn't need a function
def updateTimePlotY(group_df, var):
    return group_df[var]


def updateCells(tab_group_df, var):
    cells_output = []
    use = tab_group_df.loc[tab_group_df['variable'] == var]
    for col in use.columns:
        cells_output.append(use[col].tolist())
    return cells_output


availableGroups = df['group_name'].unique().tolist()
columns = list(df.columns)
# drop columns not used for iteration
columns.pop(columns.index('Submission Date'))
columns.pop(columns.index('group_name'))

# list for the data update dropdown
dropdown_group_selector = []

# Fill out rest of dropdown with x/y/cell data for each group
for group_choice in availableGroups:
    # subset data for table cells
    table_df_group = table_df.loc[table_df['group_name'] == group_choice]
    # subset data for scatterplot
    group_subset_df = df.loc[df['group_name'] == group_choice]
    # scatter x is same for each variable
    x_data = group_subset_df['Submission Date']
    # lists used for update method
    xargs = []
    yargs = []
    cells = []

    for i, c in enumerate(columns):
        # x and y data for scatter
        xargs.append(x_data)
        yargs.append(updateTimePlotY(group_subset_df, c))

        # table data
        celldata = updateCells(table_df_group, c)
        cells.append(dict(values=celldata))

    dropdown_group_selector.append(dict(
        args=[{'x': xargs,
               'y': yargs,
               'cells': cells,
               #'header': header # header doesn't need to change
               }],
        label=group_choice,
        method='update'
    ))

# Create dropdown for variable selection
dropdown_var_selector = []

# This is where things are weird. Before group selection, the figure has 2*ncol traces as initialized, and visible list
# should be true for [2*i, 2*i+1] to select the adjacent 2 subplots that correspond to a single variable.
# But after group selection data update, using 1*ncol and setting just [i] to True shows the correct data, although the
# legend is wrong.  For some reason, this workaround only seems to work when there are an odd number of columns to plot.
for i, c in enumerate(columns):
    vis = ([False] * (len(columns)))
    vis_indices = [i]
    #vis = ([False] * 2 * len(columns))
    #vis_indices = [2*i, 2*i+1]
    for v in vis_indices:
        vis[v] = True
    dropdown_var_selector.append(dict(
        args=[{'visible': vis}],
        label=c,
        method='restyle'
    ))

# data to initialize the traces
group = availableGroups[0]
usedf = df.loc[df['group_name'] == group]
usedf_tab = table_df.loc[table_df['group_name'] == group]

fig = make_subplots(rows=2, cols=1,
                    specs=[[{'type': 'scatter'}],
                           [{'type': 'table'}]])

# Add traces for each column
for i, c in enumerate(columns):
    vis = False
    if i == 0:
        vis = True

    # Create scatter trace
    trace = go.Scatter(x=usedf['Submission Date'], y=usedf[c],
                       mode='markers',
                       opacity=1,
                       marker_color='blue',
                       showlegend=True,
                       hovertemplate='Date: %{x}<br>Number: %{y}<extra></extra>',
                       visible=vis,
                       name=c
                       )

    # Add that trace to the figure
    fig.add_trace(trace, row=1, col=1)

    # get data for table trace
    initial_cells = updateCells(usedf_tab, c)
    # Create and add second trace for data table
    trace2 = go.Table(header=dict(values=['group', 'submission date', 'variable', 'value']),
                      cells=dict(values=initial_cells),
                      visible=vis,
                      name='table' + str(i)
                      )

    fig.add_trace(trace2, row=2, col=1)

# update a few parameters for the axes
fig.update_xaxes(title='Date')
fig.update_yaxes(title='value', rangemode='nonnegative')  # , fixedrange = True)
fig.update_layout(
    title_text='double dropdown issue',
    # hovermode = 'x',
    height=1000,
    width=850,
    title_y=0.99,
    margin=dict(t=140)
)

# Add the two dropdowns
fig.update_layout(
    updatemenus=[
        # Dropdown menu for choosing the group
        dict(
            buttons=dropdown_group_selector,
            direction='down',
            showactive=True,
            x=0.0,
            xanchor='left',
            y=1.11,
            yanchor='top'
        ),
        # and for the variables
        dict(
            buttons=dropdown_var_selector,
            direction='down',
            showactive=True,
            x=0.0,
            xanchor='left',
            y=1.06,
            yanchor='top'
        )
    ]

)

fig.show()

Ok, I’ve figured it out. I was not adding the lists of data for the update methods properly. They should be 2*N long, with the x/y data at the indices of the scatter traces, and the cell data at the indices of the table traces. My solution was to add dummy blank data in between to make sure it all lines up correctly.

I don’t know why my workaround worked at all - my best guess is something to do with the out-of-bounds indices wrapping around in the final HTML somehow.

Fixed code:

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

df1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A'], 'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03'], 'var1': [1, 1, 1], 'var2': [2, 2, 2], 'var3': [3, 3, 3]})
df2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B'], 'Submission Date': ['2023-01-04', '2023-01-05', '2023-01-06'], 'var1': [11, 11, 11], 'var2': [12, 12, 12], 'var3': [13, 13, 13]})
df = pd.concat([df1, df2])
df['Submission Date'] = pd.to_datetime(df['Submission Date'])

# dummy extra variable for testing - behavior changes when the number of variables is odd vs even(??)
#df['extravar'] = [4, 4, 4, 14, 14, 14]

# Data for the tables
tabdf1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A'], 'Submission Date': ['2023-01-01', '2023-02-11', '2023-03-05'], 'variable': ['var1', 'var2', 'var3'], 'value': ['A var1 val', 'A var2 val', 'A var3 val']})
tabdf2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B'], 'Submission Date': ['2022-06-01', '2022-07-11', '2022-08-01'], 'variable': ['var1', 'var2', 'var3'], 'value': ['B var1 val', 'B var2 val', 'B var3 val']})
table_df = pd.concat([tabdf1, tabdf2])


# fns for building data update dropdown. x is the same every time so doesn't need a function
def updateTimePlotY(group_df, var):
    return group_df[var]


def updateCells(tab_group_df, var):
    cells_output = []
    use = tab_group_df.loc[tab_group_df['variable'] == var]
    for col in use.columns:
        cells_output.append(use[col].tolist())
    return cells_output


availableGroups = df['group_name'].unique().tolist()
columns = list(df.columns)
# drop columns not used for iteration
columns.pop(columns.index('Submission Date'))
columns.pop(columns.index('group_name'))

# list for the data update dropdown
dropdown_group_selector = []

# Fill out rest of dropdown with x/y/cell data for each group
for group_choice in availableGroups:
    # subset data for table cells
    table_df_group = table_df.loc[table_df['group_name'] == group_choice]
    # subset data for scatterplot
    group_subset_df = df.loc[df['group_name'] == group_choice]
    # scatter x is same for each variable
    x_data = group_subset_df['Submission Date']
    # lists used for update method
    xargs = []
    yargs = []
    cells = []

    for i, c in enumerate(columns):
        # x and y data for scatter
        xargs.append(x_data)
        yargs.append(updateTimePlotY(group_subset_df, c))
        
        # FIX: add blank data to line up with cell traces
        xargs.append([])
        yargs.append([])

        # table data
        celldata = updateCells(table_df_group, c)
        
        # FIX: add blank data to line up with scatter traces
        cells.append(dict())
        # now add real cell data to line up with table traces
        cells.append(dict(values=celldata))

    dropdown_group_selector.append(dict(
        args=[{'x': xargs,
               'y': yargs,
               'cells': cells,
               #'header': header # header doesn't need to change
               }],
        label=group_choice,
        method='update'
    ))

# Create dropdown for variable selection
dropdown_var_selector = []

# This is where things are weird. Before group selection, the figure has 2*ncol traces as initialized, and visible list
# should be true for [2*i, 2*i+1] to select the adjacent 2 subplots that correspond to a single variable.
# But after group selection data update, using 1*ncol and setting just [i] to True shows the correct data, although the
# legend is wrong.  For some reason, this workaround only seems to work when there are an odd number of columns to plot.
for i, c in enumerate(columns):
    vis = ([False] * 2 * len(columns))
    vis_indices = [2*i, 2*i+1]
    for v in vis_indices:
        vis[v] = True
    dropdown_var_selector.append(dict(
        args=[{'visible': vis}],
        label=c,
        method='restyle'
    ))

# data to initialize the traces
group = availableGroups[0]
usedf = df.loc[df['group_name'] == group]
usedf_tab = table_df.loc[table_df['group_name'] == group]

fig = make_subplots(rows=2, cols=1,
                    specs=[[{'type': 'scatter'}],
                           [{'type': 'table'}]])

# Add traces for each column
for i, c in enumerate(columns):
    vis = False
    if i == 0:
        vis = True

    # Create scatter trace
    trace = go.Scatter(x=usedf['Submission Date'], y=usedf[c],
                       mode='markers',
                       opacity=1,
                       marker_color='blue',
                       showlegend=True,
                       hovertemplate='Date: %{x}<br>Number: %{y}<extra></extra>',
                       visible=vis,
                       name=c
                       )

    # Add that trace to the figure
    fig.add_trace(trace, row=1, col=1)

    # get data for table trace
    initial_cells = updateCells(usedf_tab, c)
    # Create and add second trace for data table
    trace2 = go.Table(header=dict(values=['group', 'submission date', 'variable', 'value']),
                      cells=dict(values=initial_cells),
                      visible=vis,
                      name='table' + str(i)
                      )

    fig.add_trace(trace2, row=2, col=1)

# update a few parameters for the axes
fig.update_xaxes(title='Date')
fig.update_yaxes(title='value', rangemode='nonnegative')  # , fixedrange = True)
fig.update_layout(
    title_text='double dropdown issue',
    # hovermode = 'x',
    height=1000,
    width=850,
    title_y=0.99,
    margin=dict(t=140)
)

# Add the two dropdowns
fig.update_layout(
    updatemenus=[
        # Dropdown menu for choosing the group
        dict(
            buttons=dropdown_group_selector,
            direction='down',
            showactive=True,
            x=0.0,
            xanchor='left',
            y=1.11,
            yanchor='top'
        ),
        # and for the variables
        dict(
            buttons=dropdown_var_selector,
            direction='down',
            showactive=True,
            x=0.0,
            xanchor='left',
            y=1.06,
            yanchor='top'
        )
    ]

)

fig.show()