A stacked bar chart divides each bar into adjoining series segments. The full bar shows a total, while its segments show category contributions; it is often used to show how trends evolve over time. section 5.2.2
In D3, build the chart as a preprocessing-and-drawing pipeline. A stack layout takes the original data and returns annotated series with a lower boundary d[0] and upper boundary d[1] for every segment. Bind each series to SVG rectangles, then use those boundaries to position and size the rectangles. section 5.0 section 5.4
Build a basic chart in this order:
- Initialize d3.stack() and provide the data keys that identify the series to stack. section 5.4
- Run the stack generator on the dataset and retain its annotated series. section 5.2
- Create a band scale for bar positions and a linear y-scale spanning zero to the maximum upper boundary. section 5.2.2
- For each series, join its values to rect elements; map x to the category, y to d[1], and height to yScale(d[0]) minus yScale(d[1]). section 5.2.2
- Add inner band padding and axes to separate bars and make values readable. section 5.2.2
Music-format sales by year
Use one bar per year and one segment per music format. A shared band scale aligns yearly positions with related charts, while a color scale distinguishes formats. The top edge of each complete bar represents the combined sales for that year. section 5.0 section 5.1.1 section 5.2.2
Stack order and offset change both appearance and interpretation. The default offset places shapes above zero; diverging separates positive and negative values, and expand converts each stack to a 0–1, 100% comparison. Recalculate the y-scale domain whenever an order or offset changes the boundaries. section 5.2.4 section 5.4
Listing 5.7 Appending the stacked bars (stacked-bars.js)
annotatedData.forEach(series => { #1
innerChart
.selectAll(`.bar-${series.key}`)
.data(series) #2
.join("rect")
.attr("class", d => `bar-${series.key}`)
.attr("x", d => xScale(d.data.year))
.attr("y", d => yScale(d[1]))
.attr("width", xScale.bandwidth())
.attr("height", d => yScale(d[0]) - yScale(d[1]))
.attr("fill", colorScale(series.key));
});
This rectangle join makes the boundary-to-geometry mapping visible: the upper boundary sets y, and the difference between boundaries sets height. section 5.2.2




