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
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://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.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"]
console.log("original data", data)
// 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 gkey = "raceethnicity" // what we group by
var xkey = "month" // the x axis
// 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 = [];
// we are making a "layer" for each race
allRaces.forEach(function(race,i) {
var xdata = {};
groups[race].forEach(function(event) {
if(!xdata[event[xkey]]) {
xdata[event[xkey]] = 1
} else {
xdata[event[xkey]]++;
}
})
// our "result" is an orered array with a count for each month
// (for the race we are currently working on)
var result = [];
months.forEach(function(g, j) {
result.push({ x: g, y: xdata[g] || 0 })
})
processed.push(result)
})
console.log("layer data", processed)
var n = allRaces.length, // number of layers
m = processed.length, // number of samples per layer
stack = d3.layout.stack();
var layers = stack(processed); // calculate the stack layout
var yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); })
var yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
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.scale.ordinal()
.domain(months)
.rangeRoundBands([0, width], .08);
var y = d3.scale.linear()
.domain([0, yStackMax])
.range([height, 0]);
var color = d3.scale.category20c()
.domain([0, n-1])
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
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.x); })
.attr("y", height)
.attr("width", x.rangeBand())
.attr("height", 0);
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
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, i, j) { return x(d.x) + x.rangeBand() / n * j; })
.attr("width", x.rangeBand() / n)
.transition()
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
}
function transitionStacked() {
y.domain([0, yStackMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.transition()
.attr("x", function(d) { return x(d.x); })
.attr("width", x.rangeBand());
}
});
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js