2025, Dec 31 23:00
Dynamic grouped and stacked bar charts in Plotly: two-bar groups with independent per-side stacks
Learn how to build dynamic grouped + stacked bar charts in Plotly (Python): two bars per category with independent stacks using offsetgroup and calculated bases
Building a grouped + stacked bar chart in Plotly is straightforward until you need each side of a category to stack different components dynamically. The common pattern stacks the same components on the same side every time, which becomes a limitation once you introduce a second parameter per category, such as yes on the left and no on the right, and want any component to appear on either side with independent values.
Baseline: the grouped + stacked pattern
Here is a minimal setup that groups bars per category and stacks on one side only. It demonstrates the conventional approach, where the left bar is a single series and the right bar is stacked from two series. This is the pattern many start from, but it hardcodes which components stack on which side.
from plotly import graph_objects as pgo
sample = {
"original": [15, 23, 32, 10, 23],
"model_1": [4, 8, 18, 6, 0],
"model_2": [11, 18, 18, 0, 20],
"labels": [
"feature",
"question",
"bug",
"documentation",
"maintenance"
]
}
chart = pgo.Figure(
data=[
pgo.Bar(
name="Original",
x=sample["labels"],
y=sample["original"],
offsetgroup=0,
),
pgo.Bar(
name="Model 1",
x=sample["labels"],
y=sample["model_1"],
offsetgroup=1,
),
pgo.Bar(
name="Model 2",
x=sample["labels"],
y=sample["model_2"],
offsetgroup=1,
base=sample["model_1"],
)
],
layout=pgo.Layout(
title="Issue Types - Original and Models",
yaxis_title="Number of Issues"
)
)
What’s the limitation?
This configuration assumes a fixed split: the left bar is always one component, and the right bar is always a stack of two others. That won’t work when you need both sides to be stacks, and, more importantly, when any of the components can appear on either side with category-specific values. The requirement is to keep exactly two bars per category while allowing each individual bar to contain any combination of segments in a consistent color scheme.
Approach: dynamic stacking with calculated bases
The key is to render each side as its own stack, computing the base per category and per segment. Color must remain consistent by component across both sides. The total shape remains a two-bar group per category, which keeps the layout stable and the count to ten bars overall for five categories.
If you’re looking for more background on the grouped + stacked pattern in Plotly, see the related discussion: stacked + grouped bar chart.
Solution: two independently stacked sides per category
The snippet below constructs left and right stacks from dictionaries per category, computing the base progressively. It keeps legend entries clean by grouping identical components and showing each only once.
from plotly import graph_objects as pgo
payload = {
"left": [
{"original": 15, "model_1": 4, "model_2": 11},
{"original": 23, "model_1": 8, "model_2": 18},
{"original": 32, "model_1": 18, "model_2": 18},
{"original": 10, "model_1": 6, "model_2": 0},
{"original": 23, "model_1": 0, "model_2": 20}
],
"right": [
{"original": 7, "model_1": 3, "model_2": 5},
{"original": 12, "model_1": 6, "model_2": 8},
{"original": 15, "model_1": 8, "model_2": 10},
{"original": 5, "model_1": 4, "model_2": 0},
{"original": 10, "model_1": 2, "model_2": 12}
],
"labels": [
"feature",
"question",
"bug",
"documentation",
"maintenance"
]
}
viz = pgo.Figure()
palette = {
"original": "#636EFA",
"model_1": "#EF553B",
"model_2": "#00CC96"
}
def append_stacks(side_key, group_id):
side_vals = payload[side_key]
for idx, cat in enumerate(payload["labels"]):
level = 0
for segment in ["original", "model_1", "model_2"]:
amount = side_vals[idx].get(segment, 0)
if amount > 0:
viz.add_trace(pgo.Bar(
name=f"{segment.replace('_', ' ').title()} ({side_key.title()})",
x=[cat],
y=[amount],
offsetgroup=group_id,
base=level,
marker_color=palette[segment],
legendgroup=segment,
showlegend=(idx == 0)
))
level += amount
append_stacks("left", 0)
append_stacks("right", 1)
viz.update_layout(
title="Issue Types - Original and Models",
yaxis_title="Number of Issues",
barmode="group",
legend_title="Model Types",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
)
)
viz.show()
Why this works
Each bar in a category belongs to its own offsetgroup, which guarantees two bars per category. Within a bar, stacking is driven by base values recalculated per category and per segment in a deterministic order. This allows the same component to appear on either side with different values, while colors remain consistent by component. Legend grouping avoids duplicates, preserving a clean legend regardless of how many stacked segments are rendered.
Why it matters
When visualizing model comparisons or split outcomes, being able to stack per side without increasing the number of bars per category is essential for readability. It keeps the chart compact and comparable across categories, while letting you show richer composition per bar. This pattern scales with additional categories and remains robust even when some segments are zero on one side and non-zero on the other.
Takeaways
Use two offset groups to represent the left and right sides of each category. Compute base values incrementally to create stacks within each bar. Keep color mapping consistent for recognizability, and group legend entries to avoid repetition. With this setup, you preserve the two-bars-per-category structure while supporting flexible, data-driven stacking on both sides.