D3 Version 4 implementation of the stacked to grouped bar chart.
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. Colors and data generation inspired by Byron and Wattenberg.
Police killings data for 2015 (up to June) downloaded from FiveThirtyEight
forked from mbostock's block: Stacked-to-Grouped Bars
forked from enjalot's block: Stacked-to-Grouped Police Killings
forked from Nastasi7's block: Stacked-to-Grouped Police Killings
forked from mashehu's block: Stacked-to-Grouped Police Killings with D3 v4
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
position: relative;
width: 960px;
}
text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
</style>
<form>
<label><input type="radio" name="mode" value="grouped"> Grouped</label>
<label><input type="radio" name="mode" value="stacked" checked> Stacked</label>
</form>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
d3.csv("police_killings.csv", function(err, data) {
var allRaces = ["Black","Hispanic/Latino", "White", "Unknown", "Asian/Pacific Islander", "Native American"];
var months = ["January", "February", "March", "April", "May", "June"]
// we want to "pivot" our data into deaths by month by race
// this is a rather ugly way to do it in javascript. would probably be easier
// to group the data in another tool (excel, google sheets, etc) and load that
var groups = {}
var races = {};
var xkey = "raceethnicity" // the x axis
var gkey = "month" // what we group by
// first we group all the events by race
data.forEach(function(d) {
if(!groups[d[gkey]]) {
groups[d[gkey]] = [d];
} else {
groups[d[gkey]].push(d)
}
})
var processed = [];
//Next we count how many incidents happended for each month
months.forEach(function(month,i) {
var xdata = {};
groups[month].forEach(function(event) {
if(!xdata[event[xkey]]) {
xdata[event[xkey]] = 1
} else {
xdata[event[xkey]]++;
}
})
// our "result" is an ordered array with a count for each month
// (for the race we are currently working on)
var result = {};
allRaces.forEach(function(g) {
result[g]= xdata[g]||0;
})
processed.push(result)
})
var n = allRaces.length, // number of layers
m = processed.length, // number of samples per layer
stack = d3.stack().keys(allRaces);
var layers = stack(processed); // calculate the stack layout
layers.forEach(function(d,i) { //adding keys to every datapoint
d.forEach(function(dd,j){
dd.month = months[j];
dd.race = allRaces[i];
})
});
var yGroupMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d[1] - d[0];
});
}),
yStackMax = d3.max(layers, function(layer) {
return d3.max(layer, function(d) {
return d[1];
});
});
var margin = {top: 40, right: 10, bottom: 20, left: 10},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleBand()
.domain(months)
.rangeRound([0, width])
.padding(0.08);
var y = d3.scaleLinear()
.domain([0, yStackMax])
.range([height, 0]);
var z = d3.scaleBand().domain(allRaces).rangeRound([0, x.bandwidth()]);
var color = d3.scaleOrdinal(d3.schemeCategory20c)
.domain([0, n-1])
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
var rect = layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) {
return x(d.month); })
.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]);
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(0));
var legend = svg.selectAll(".legend")
.data(allRaces)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d,i) { return color(i) });
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
d3.selectAll("input").on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"grouped\"]").property("checked", true).each(change);
}, 2000);
function change() {
clearTimeout(timeout);
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yGroupMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d) {
return x(d.month)+ z(d.race);
})
.attr("width", x.bandwidth() / m)
.transition()
.attr("y", function(d) {
return y(d.data[d.race]);
})
.attr("height", function(d) {
return height - y(d.data[d.race]);
});
}
function transitionStacked() {
y.domain([0, yStackMax]);
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) { return x(d.month); })
.attr("width", x.bandwidth());
}
});
</script>
Modified http://d3js.org/d3.v4.min.js to a secure url
https://d3js.org/d3.v4.min.js