Reputation: 21
I’ve been attempting to make a non-Dash interactive HTML plot with dual dropdown. The product I'm trying to create is an HTML figure with two subplots (one scatter plot and one table of values) and two dropdown menus. The first dropdown is used to filter the data based on values in a specific column, and the second dropdown selects which variable/column is displayed in the subplots. I’ve used this example as a basic template and tweaked it to fit the format I want. This works as intended with just one plot displayed at a time, but when I incorporate a second subplot I start getting unexpected results and the wrong traces/labels are displayed. I’ve included code for a toy example reproduction below.
Based on the example linked above, for the group filtering I use the 'update' method to change the underlying x, y, and cell data for the traces. For the variable selection, I use a 'restyle' method to choose which traces are visible for a given selection.
For N variables with 2 subplots, and only an adjacent pair of traces (one for each subplot) visible at a time, it seems as though my list of traces should be 2*N long. 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 filtering 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 index i set to True, although the initialized traces (before group selection) and legend labels aren’t ordered correctly, 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 (you can try this yourself by uncommenting line 12) .
So my current workaround is: make sure to use an odd number of variables to plot, initialize the charts with blank data to force an initial group and variable selection, use a visibility parameter list in my restyle dropdown of only 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.
Another possibly related question I have: is it possible to use updatemenus to update two different sets of x and y data, like for two scatter subplots separately? My update args are 'x', 'y', and 'cells'. But is it possible to specify the target subplot with something like 'x1', 'y1', 'x2', 'y2' instead? Or is all of this beyond the capabilities of just HTML and I'll need to move to Dash?
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
dfA = pd.DataFrame({'group_name': ['group A', 'group A', 'group A', 'group A'], 'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04'], 'var1': [1, 1, 1, 1], 'var2': [2, 2, 2, 2], 'var3': [3, 3, 3, 3]})
dfB = pd.DataFrame({'group_name': ['group B', 'group B', 'group B', 'group B'], 'Submission Date': ['2023-01-05', '2023-01-06', '2023-01-07', '2023-01-08'], 'var1': [11, 12, 13, 14], 'var2': [12, 16, 14, 13], 'var3': [13, 12, 11, 12]})
dfC = pd.DataFrame({'group_name': ['group C', 'group C', 'group C', 'group C'], 'Submission Date': ['2023-01-09', '2023-01-10', '2023-01-11', '2023-01-12'], 'var1': [21, 15, 22, 16], 'var2': [22, 23, 21, 20], 'var3': [23, 23, 16, 16]})
df = pd.concat([dfA, dfB, dfC])
df['Submission Date'] = pd.to_datetime(df['Submission Date'])
# dummy extra variable for testing - behavior changes when the number of columns is odd vs even(??)
#df['dummy variable 4'] = [4, 4, 4, 4, 14, 14, 14, 14, 25, 25, 25, 25]
# dfs for table data used in subplot 2
tabdf1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A', 'group A'],
'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['A val 1', 'A val 2', 'A val 3', 'A val 4']})
tabdf2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B', 'group B'],
'Submission Date': ['2022-06-01', '2022-07-11', '2022-08-01', '2022-09-01'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['B val 1', 'B val 2', 'B val 3', 'B val 4']})
tabdf3 = pd.DataFrame({'group_name': ['group C', 'group C', 'group C', 'group C'],
'Submission Date': ['2022-01-01', '2022-02-01', '2022-03-01', '2022-04-01'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['C val 1', 'C val 2', 'C val 3', 'C val 4']})
table_df = pd.concat([tabdf1, tabdf2, tabdf3])
# fns for building data update dropdown.
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 = []
# Create initial blank value to force group selection
dropdown_group_selector.append(dict(
args=[{'x': [],
'y': [],
'cells': [],
}],
label='Select Group',
method='update'
))
# 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]
# select column for x data
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,
}],
label=group_choice,
method='update'
))
# Create dropdown for variable selection
dropdown_var_selector = []
# Create initial blank selection to force initial choice
dropdown_var_selector.append(dict(
args=[{'visible': False}],
label='Select Variable',
method='restyle'
))
# Fill out variable selection dropdown
for i, c in enumerate(columns):
# why does this work...
vis = ([False] * (len(columns)))
vis[i] = True
# ...instead of this?
# vis = ([False] * 2 * len(columns))
# for v in [2*i, 2*i+1]:
# 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()
Upvotes: 1
Views: 315
Reputation: 21
I've figured it out. I was not passing the right length of data lists in the group dropdown update method. Since my figure has 2*N traces alternating between scatter and table, I need to make sure the x and y arguments in the update method line up with the scatter traces, and the cell arguments line up with the table traces. The fix is adding dummy blank x/y data at the indices of the cell traces, and dummy blank cell data at the indices of the scatter traces. Then the data lines up with the correct traces and everything works as intended.
I have absolutely no idea how or why my previous workaround functioned at all. My best guess is that it has something to do with out-of-bounds indices rolling back over in the rendered plot, but figuring out exactly what went on there is beyond my skill set.
Fixed code:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
dfA = pd.DataFrame({'group_name': ['group A', 'group A', 'group A', 'group A'],
'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04'], 'var1': [1, 1, 1, 1],
'var2': [2, 2, 2, 2], 'var3': [3, 3, 3, 3]})
dfB = pd.DataFrame({'group_name': ['group B', 'group B', 'group B', 'group B'],
'Submission Date': ['2023-01-05', '2023-01-06', '2023-01-07', '2023-01-08'],
'var1': [11, 12, 13, 14], 'var2': [12, 16, 14, 13], 'var3': [13, 12, 11, 12]})
dfC = pd.DataFrame({'group_name': ['group C', 'group C', 'group C', 'group C'],
'Submission Date': ['2023-01-09', '2023-01-10', '2023-01-11', '2023-01-12'],
'var1': [21, 15, 22, 16], 'var2': [22, 23, 21, 20], 'var3': [23, 23, 16, 16]})
df = pd.concat([dfA, dfB, dfC])
df['Submission Date'] = pd.to_datetime(df['Submission Date'])
# dummy extra variable for testing - behavior changes when the number of columns is odd vs even(??)
#df['dummy variable 4'] = [4, 4, 4, 4, 14, 14, 14, 14, 25, 25, 25, 25]
# dfs for table data used in subplot 2
tabdf1 = pd.DataFrame({'group_name': ['group A', 'group A', 'group A', 'group A'],
'Submission Date': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['A val 1', 'A val 2', 'A val 3', 'A val 4']})
tabdf2 = pd.DataFrame({'group_name': ['group B', 'group B', 'group B', 'group B'],
'Submission Date': ['2022-06-01', '2022-07-11', '2022-08-01', '2022-09-01'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['B val 1', 'B val 2', 'B val 3', 'B val 4']})
tabdf3 = pd.DataFrame({'group_name': ['group C', 'group C', 'group C', 'group C'],
'Submission Date': ['2022-01-01', '2022-02-01', '2022-03-01', '2022-04-01'],
'variable': ['var1', 'var2', 'var3', 'var4'],
'value': ['C val 1', 'C val 2', 'C val 3', 'C val 4']})
table_df = pd.concat([tabdf1, tabdf2, tabdf3])
# fns for building data update dropdown.
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 = []
# Create initial blank value to force group selection
dropdown_group_selector.append(dict(
args=[{'x': [],
'y': [],
'cells': [],
}],
label='Select Group',
method='update'
))
# 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]
# select column for x data
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 x and y data to line up with cell traces
xargs.append([])
yargs.append([])
# table data
celldata = updateCells(table_df_group, c)
# FIX: add blank cell data to line up with scatter traces
cells.append(dict())
# then add cell data
cells.append(dict(values=celldata))
dropdown_group_selector.append(dict(
args=[{'x': xargs,
'y': yargs,
'cells': cells,
}],
label=group_choice,
method='update'
))
# Create dropdown for variable selection
dropdown_var_selector = []
# Create initial blank selection to force initial choice
dropdown_var_selector.append(dict(
args=[{'visible': False}],
label='Select Variable',
method='restyle'
))
# Fill out variable selection dropdown
for i, c in enumerate(columns):
vis = ([False] * 2 * len(columns))
for v in [2*i, 2*i+1]:
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()
Upvotes: 1