block by enjalot 2b1446f496c3968f6ecd

Stacked-to-Grouped Police Killings

Full Screen

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

index.html

<!DOCTYPE html>
<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>