So, I found a sort of hacky solution by manually setting the widths of the bars depending on the number of groups:
num_groups = df2['Group'].nunique()
print('num_groups=\n', num_groups)
b = 0.6
d = 0.5
budget_width = b/num_groups + b
data_width = d/num_groups + d
if num_groups == 1:
budget_width = b
data_width = d
I found this to work pretty well. You can play around with the values of ‘b’ and ‘d’ for the widths of each bar. Here’s an example plot:
I’ll mark this as solved, although I still don’t fully understand why this works.
plotly.graph_objects.Figure — 5.20.0 documentation.
bargap: “Sets the gap (in plot fraction) between bars of adjacent location coordinates.”
For some reason, setting this to 1.0 results in the overlapping behavior that I want. I guess it considers bars within the same group to be at adjacent locations. But the behavior is counter-intuitive. (why does a bargap of 0.0 mean the edges are touching, while a bargap of 1.0 makes them overlap?)
Meanwhile, try setting ‘bargroupgap’ and you’ll find it does absolutely nothing in this example…
EDIT:
I cleaned up the code a bit.
I’ll post the full example code here so people can play around with it if they want:
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
# from app import num_to_month
# Get test data
def get_test_data(num_groups: int):
if num_groups not in [1, 2, 3, 4, 5, 6]:
raise ValueError('Invalid range for num_groups (must be in [1,6])')
##############################################
# 6 groups
if num_groups == 6:
dummy = ['', '', '', '', '', '', '', '', '', '', '', '']
field = ['Month', 'Category', 'Category', 'Category', 'Category', 'Category', 'Category', 'Month', 'Category',
'Category', 'Category', 'Category']
group = ['2', 'Misc', 'Insurance', 'Housing', 'Food', 'Transportation', 'Subscriptions', '3', 'Misc',
'Subscriptions',
'Transportation', 'Food']
amount = [-2046.67, -646.61, -162.25, -840.43, -307.03, -71.37, -18.98, -686.06, -365.82, -37.64, -34.28,
-248.32]
budget = [-2263, -500, -163, -850, -500, -200, -50, -2263, -500, -50, -200, -500]
percent = [90.4406, 129.322, 99.5399, 98.8741, 61.406, 35.685, 37.96, 30.3164, 73.164, 75.28, 17.14, 49.664]
margin = [-216.33, 146.61, -0.75, -9.57, -192.97, -128.63, -31.02, -1576.94, -134.18, -12.36, -165.72, -251.68]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C',
'#2CA02C',
'#2CA02C', '#2CA02C']
# 5 groups
if num_groups == 5:
dummy = ['', '', '', '', '', '', '', '', '', '', '']
field = ['Month', 'Category', 'Category', 'Category', 'Category', 'Category', 'Month', 'Category',
'Category', 'Category', 'Category']
group = ['2', 'Misc', 'Housing', 'Food', 'Transportation', 'Subscriptions', '3', 'Misc', 'Subscriptions',
'Transportation', 'Food']
amount = [-2046.67, -646.61, -840.43, -307.03, -71.37, -18.98, -686.06, -365.82, -37.64, -34.28, -248.32]
budget = [-2263, -500, -850, -500, -200, -50, -2263, -500, -50, -200, -500]
percent = [90.4406, 129.322, 98.8741, 61.406, 35.685, 37.96, 30.3164, 73.164, 75.28, 17.14, 49.664]
margin = [-216.33, 146.61, -9.57, -192.97, -128.63, -31.02, -1576.94, -134.18, -12.36, -165.72, -251.68]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C',
'#2CA02C', '#2CA02C']
# 4 groups
if num_groups == 4:
dummy = ['', '', '', '', '', '', '', '', '']
field = ['Month', 'Category', 'Category', 'Category', 'Category', 'Month', 'Category',
'Category', 'Category']
group = ['2', 'Misc', 'Housing', 'Food', 'Subscriptions', '3', 'Misc', 'Subscriptions', 'Food']
amount = [-2046.67, -646.61, -840.43, -307.03, -18.98, -686.06, -365.82, -37.64, -248.32]
budget = [-2263, -500, -850, -500, -50, -2263, -500, -50, -500]
percent = [90.4406, 129.322, 98.8741, 61.406, 37.96, 30.3164, 73.164, 75.28, 49.664]
margin = [-216.33, 146.61, -9.57, -192.97, -31.02, -1576.94, -134.18, -12.36, -251.68]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C']
# 3 groups
if num_groups == 3:
dummy = ['', '', '', '', '', '', '', '']
field = ['Month', 'Category', 'Category', 'Category', 'Month', 'Category',
'Category', 'Category']
group = ['2', 'Misc', 'Food', 'Subscriptions', '3', 'Misc', 'Subscriptions', 'Food']
amount = [-2046.67, -646.61, -307.03, -18.98, -686.06, -365.82, -37.64, -248.32]
budget = [-2263, -500, -500, -50, -2263, -500, -50, -500]
percent = [90.4406, 129.322, 61.406, 37.96, 30.3164, 73.164, 75.28, 49.664]
margin = [-216.33, 146.61, -192.97, -31.02, -1576.94, -134.18, -12.36, -251.68]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C']
# 2 groups
if num_groups == 2:
dummy = ['', '', '', '', '', '']
field = ['Month', 'Category', 'Category', 'Month', 'Category', 'Category']
group = ['2', 'Misc', 'Food', '3', 'Misc', 'Food']
amount = [-2046.67, -646.61, -307.03, -686.06, -365.82, -248.32]
budget = [-2263, -500, -500, -2263, -500, -500]
percent = [90.4406, 129.322, 61.406, 30.3164, 73.164, 49.664]
margin = [-216.33, 146.61, -192.97, -1576.94, -134.18, -251.68]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C', '#2CA02C', '#2CA02C']
# 1 group
if num_groups == 1:
dummy = ['', '', '', '']
field = ['Month', 'Category', 'Month', 'Category']
group = ['2', 'Misc', '3', 'Misc']
amount = [-2046.67, -646.61, -686.06, -365.82]
budget = [-2263, -500, -2263, -500]
percent = [90.4406, 129.322, 30.3164, 73.164]
margin = [-216.33, 146.61, -1576.94, -134.18]
color = ['#2CA02C', '#D62728', '#2CA02C', '#2CA02C']
##############################################
# Create dataframe from test data
data_dict = {
'Dummy': dummy,
'Field': field,
'Group': group,
'Amount': amount,
'Budget': budget,
'Percent': percent,
'Margin': margin,
'Color': color,
}
df = pd.DataFrame(data_dict)
# df['Group'] = df.apply(num_to_month, axis=1)
# Copy and remove unnecessary data
df_test = df.copy()
df_test = df_test.loc[df_test['Field'] == 'Category']
print('df_test=\n', df_test)
return df_test
# Create plotly figure from params
def make_figure(df2=None, makeStacked=False) -> go.Figure:
# figure
fig = go.Figure()
# common settings
budget_barmode = 'relative' # 'group', 'overlay' or 'relative' (default 'relative')
data_barmode = 'relative' # 'group', 'overlay' or 'relative' (default 'relative')
fig_barmode = 'group' # 'stack', 'relative', 'group', 'overlay' (default 'group')
custom_data = ['Group', 'Budget', 'Percent', 'Margin']
# bar gap
bargap = 1.0 # in range [0,1]
# bargroupgap = 0.0 # does absolutely nothing
showlegend = False
# opacity and bar width
budget_opacity = 0.5
budget_default_width = 0.6 # 0.6
data_default_width = 0.5 # 0.5
# Whether plot is stacked or grouped
if makeStacked:
x = df2['Dummy']
title = 'Budget (Stacked)'
x_title = ''
budget_width = budget_default_width
data_width = data_default_width
else:
x = df2['Group']
title = 'Budget (Grouped)'
x_title = 'Group'
num_groups = df2['Group'].nunique()
print('num_groups=\n', num_groups)
# slight adjustment to bar width for large number of groups
if num_groups > 2:
budget_default_width += 0.03 * num_groups
data_default_width += 0.03 * num_groups
budget_width = (budget_default_width / num_groups) + budget_default_width
data_width = (data_default_width / num_groups) + data_default_width
if num_groups == 1:
budget_width = budget_default_width
data_width = data_default_width
##############################################
# Make Traces
# budget trace
fig1 = px.bar(
data_frame=df2,
x=x,
y=df2['Budget'],
custom_data=custom_data,
opacity=budget_opacity,
barmode=budget_barmode, # 'group', 'overlay' or 'relative' (default 'relative')
).update_traces(
width=budget_width,
hovertemplate="<br>".join([
"Group: %{customdata[0]}",
"Budget: $%{customdata[1]:.0f}",
]),
# hovertemplate=None,
# hoverinfo='skip'
)
fig1.data[0]['showlegend'] = True
fig1.data[0]['name'] = 'Budget'
fig1.data[0]['hoverlabel'].namelength = 0
# data trace
fig2 = px.bar(
data_frame=df2,
x=x,
y=df2['Amount'],
custom_data=custom_data,
barmode=data_barmode, # 'group', 'overlay' or 'relative' (default 'relative')
).update_traces(
width=data_width,
marker_color=df2['Color'],
hovertemplate="<br>".join([
"Group: %{customdata[0]}",
"Amount: $%{y}",
"Budget: $%{customdata[1]:.0f}",
"Percent: %{customdata[2]:.1f}%",
"Margin: $%{customdata[3]}",
]),
)
fig2.data[0]['showlegend'] = True
fig2.data[0]['name'] = 'Data'
fig2.data[0]['hoverlabel'].namelength = 0
##############################################
# Add traces to chart
fig.add_traces([fig1.data[0], fig2.data[0]])
# Update figure layout
fig.update_layout(
title=title,
title_x=0.5,
xaxis_title=x_title,
yaxis_title='Amount',
showlegend=showlegend,
barmode=fig_barmode, # 'stack', 'relative', 'group', 'overlay' (default 'relative'?)
bargap=bargap,
#bargroupgap=bargroupgap,
)
return fig
if __name__ == '__main__':
for i in range(1, 7):
# Get test data
df_test = get_test_data(num_groups=i)
# Make figures
# fig_stacked = make_figure(df2=df_test, makeStacked=True)
# fig_stacked.show()
fig_grouped = make_figure(df2=df_test, makeStacked=False)
fig_grouped.show()