block by zachmargolis 9315833

Stack-to-Split Transition

Full Screen

Stack-Split Transition

Transitions between stacked and split (small multiples) area charts.

index.html

<!doctype html>
<meta charset="utf-8" />
<style>

body {
  font: 10pt/12pt "Helvetica", sans-serif;
}

form {
  position: absolute;
  top: 1em;
  left: 1em;
}

.x-axis path, .y-axis path {
  fill: none;
  stroke: #555;
  shape-rendering: crispEdges;
}

.area {
  fill: #ccc;
  stroke: none;
}

</style>

<script src="//d3js.org/d3.v3.min.js"></script>

<form>
  <label for="stack">
    <input type="radio" id="stack" name="layout" value="stack" />
    Stack
  </label>
  <label for="stack">
    <input type="radio" id="split" name="layout" value="split" />
    Split
  </label>
</form>

<script>

var width = 960,
    height = 500,
    margin = { top: 10, bottom: 25, left: 10, right: 50 }, // around the graph
    spacing = { bottom: 15, right: 5 },
    n = 8, // number of layers
    m = 50, // numbe of samples per layer,
    stack = d3.layout.stack(),
    data = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })),
    ySplitMax = d3.max(data, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
    yStackMax = d3.max(data, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); }),
    duration = 1000,
    isTransition = false;

    svg = d3.select('body').append('svg')
      .attr('width', width)
      .attr('height', height);

var layout = 'stack';

function translate(x, y) {
  return "translate(" + x + ", " + y + ")";
}

function update(firstTime) {
  function rowHeight() {
    return Math.floor((height - margin.top - margin.bottom - (n - 1) * spacing.bottom) / n);
  };

  var lastRow = n - 1;

  var x = d3.scale.linear()
    .range([margin.left, width - margin.right])
    .domain([0, m-1]);
  var xAxis = d3.svg.axis()
    .scale(x)
    .orient('bottom');

  var y = d3.scale.linear()

  if (layout == 'stack') {
    y.range([height - margin.top - margin.bottom, 0])
      .domain([0, yStackMax]);
  } else {
    y.range([rowHeight(), 0])
      .domain([0, ySplitMax])
  }

  yAxis = d3.svg.axis()
    .scale(y)
    .orient("right")
    .ticks(layout == 'split' ? 3 : 10);

  if (firstTime) {
    svg.append('g')
      .attr('class', 'x-axis')
      .attr('transform', translate(0, height - margin.top - margin.bottom + 5))
      .call(xAxis)
  }

  var layers = svg.selectAll('g.layer')
    .data(data, function(d) { return data.indexOf(d) });

  var enterLayers = layers.enter()
    .append('g')
      .attr('class', 'layer')
      .attr('width', width)
      .attr('transform', function(d, i) {
        if (layout == 'split') {
          return translate(0, (lastRow - i) * (rowHeight() + spacing.bottom));
        }
      });

  layers.exit().remove()

  layers.transition()
    .duration(duration)
    .attr('transform', function(d, i) {
      if (layout == 'stack') {
        return translate(0, 0);
      } else {
        return translate(0, (lastRow - i) * (rowHeight() + spacing.bottom));
      }
    })
    .each('start', function() { isTransition = true })
    .each('end', function() { isTransition = false });

  var zeroArea = d3.svg.area()
    .x(function(d) { return x(d.x) })
    .y0(y(0))
    .y1(y(0));

  var grayGradient = d3.interpolate('#666', '#ddd');

  function color(d, i) {
    i = data.indexOf(d);
    return grayGradient(i / n);
  }

  var areas = enterLayers.append('path')
    .attr('class', 'area')
    .attr('d', zeroArea)
    .style('fill', color)
    .style('stroke', color)
    .style('stroke-width', 1) // fill gaps between layers
    .on('mouseover', function() {
      !isTransition && d3.select(this).transition()
        .style('fill', '#d66').style('stroke', '#d66');
    })
    .on('mouseout', function() {
      !isTransition && d3.select(this).transition()
        .style('fill', color).style('stroke', color);
    });
  var area = d3.svg.area()
    .x(function(d) { return x(d.x) });

  if (layout == 'stack') {
    area.y0(function(d) { return y(d.y0); })
      .y1(function(d) { return y(d.y0 + d.y); });
  } else {
    area.y0(y(0))
      .y1(function(d) { return y(d.y)} );
  }

  layers.selectAll('path.area').transition()
    .duration(duration)
    .attr('d', area);

  enterLayers.append('g')
    .attr('class', 'y-axis')
    .attr('transform', translate(width - margin.right + spacing.right, 0))
    .attr('opacity', 0)

  var yAxes = layers.selectAll('g.y-axis');
  yAxes.transition()
    .duration(duration)
    .attr('opacity', function(d, i) {
      i = data.indexOf(d);
      if (layout == 'stack') {
        return (i == 0) ? 1 : 0;
      } else {
        return 1;
      }
    })
    .call(yAxis);
}

// Inspired by Lee Byron's test data generator.
// Borrowed from //bl.ocks.org/mbostock/3943967
function bumpLayer(n, o) {

  function bump(a) {
    var x = 1 / (.1 + Math.random()),
        y = 2 * Math.random() - .5,
        z = 10 / (.1 + Math.random());
    for (var i = 0; i < n; i++) {
      var w = (i / n - y) * z;
      a[i] += x * Math.exp(-w * w);
    }
  }

  var a = [], i;
  for (i = 0; i < n; ++i) a[i] = o + o * Math.random();
  for (i = 0; i < 5; ++i) bump(a);
  return a.map(function(d, i) { return {x: i, y: Math.max(0, d)}; });
}

update(true)

setTimeout(function() {
  d3.select('input#stack').attr('checked', 'checked');
}, duration);

d3.selectAll('input').on('change', function() {
  var e = d3.select(this);
  if (e.attr('value') == 'stack' && e.attr('checked')) {
    layout = 'stack';
  } else {
    layout = 'split';
  }
  update()
});

</script>