Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active November 21, 2019 20:19
  • Star 8 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save mbostock/3943967 to your computer and use it in GitHub Desktop.
Stacked-to-Grouped Bars
license: gpl-3.0
redirect: https://observablehq.com/@d3/d3-stacked-to-grouped-bars

Switch between stacked and grouped layouts using sequenced transitions! Animations preserve object constancy and allow the user to follow the data across views. Animation design by Heer and Robertson. Inspired by Byron and Wattenberg.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
form {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
position: absolute;
left: 10px;
top: 10px;
}
label {
display: block;
}
</style>
<form>
<label><input type="radio" name="mode" value="grouped"> Grouped</label>
<label><input type="radio" name="mode" value="stacked" checked> Stacked</label>
</form>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var n = 4, // The number of series.
m = 58; // The number of values per series.
// The xz array has m elements, representing the x-values shared by all series.
// The yz array has n elements, representing the y-values of each of the n series.
// Each yz[i] is an array of m non-negative numbers representing a y-value for xz[i].
// The y01z array has the same structure as yz, but with stacked [y₀, y₁] instead of y.
var xz = d3.range(m),
yz = d3.range(n).map(function() { return bumps(m); }),
y01z = d3.stack().keys(d3.range(n))(d3.transpose(yz)),
yMax = d3.max(yz, function(y) { return d3.max(y); }),
y1Max = d3.max(y01z, function(y) { return d3.max(y, function(d) { return d[1]; }); });
var svg = d3.select("svg"),
margin = {top: 40, right: 10, bottom: 20, left: 10},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand()
.domain(xz)
.rangeRound([0, width])
.padding(0.08);
var y = d3.scaleLinear()
.domain([0, y1Max])
.range([height, 0]);
var color = d3.scaleOrdinal()
.domain(d3.range(n))
.range(d3.schemeCategory20c);
var series = g.selectAll(".series")
.data(y01z)
.enter().append("g")
.attr("fill", function(d, i) { return color(i); });
var rect = series.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("y", height)
.attr("width", x.bandwidth())
.attr("height", 0);
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); });
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickSize(0)
.tickPadding(6));
d3.selectAll("input")
.on("change", changed);
var timeout = d3.timeout(function() {
d3.select("input[value=\"grouped\"]")
.property("checked", true)
.dispatch("change");
}, 2000);
function changed() {
timeout.stop();
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d, i) { return x(i) + x.bandwidth() / n * this.parentNode.__data__.key; })
.attr("width", x.bandwidth() / n)
.transition()
.attr("y", function(d) { return y(d[1] - d[0]); })
.attr("height", function(d) { return y(0) - y(d[1] - d[0]); });
}
function transitionStacked() {
y.domain([0, y1Max]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.transition()
.attr("x", function(d, i) { return x(i); })
.attr("width", x.bandwidth());
}
// Returns an array of m psuedorandom, smoothly-varying non-negative numbers.
// Inspired by Lee Byron’s test data generator.
// http://leebyron.com/streamgraph/
function bumps(m) {
var values = [], i, j, w, x, y, z;
// Initialize with uniform random values in [0.1, 0.2).
for (i = 0; i < m; ++i) {
values[i] = 0.1 + 0.1 * Math.random();
}
// Add five random bumps.
for (j = 0; j < 5; ++j) {
x = 1 / (0.1 + Math.random());
y = 2 * Math.random() - 0.5;
z = 10 / (0.1 + Math.random());
for (i = 0; i < m; i++) {
w = (i / m - y) * z;
values[i] += x * Math.exp(-w * w);
}
}
// Ensure all values are positive.
for (i = 0; i < m; ++i) {
values[i] = Math.max(0, values[i]);
}
return values;
}
</script>
@jaishivn
Copy link

Hey Mike,

Sorry i am a beginner in D3 concepts. I am currently using your Stacked Bar chart to learn and understand the D3 Concepts. I am just playing with it and struck with a problem recently. I want to click a bar and display it's value when it is clicked. Since the data is grouped into layers i couldn't sort out how to retrieve the values of bars and display it. Could you please help me in sorting out this issue? Help appreciated. Thanks!

@HaozheW
Copy link

HaozheW commented Apr 21, 2015

@igs52ru
Copy link

igs52ru commented Jul 7, 2016

I noob in IS and D3 and don't migrate this example from version 3 to 4.0 (only stacked bar part).
v3 https://jsfiddle.net/9y2g65qc/
v4 https://jsfiddle.net/9y2g65qc/10/
Could you please help me? Thanks!

@danielepalumbo89
Copy link

Hi everyone,
Do you have a version of this chart realised not with random data but with external CSV data? Is it possible?
Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment