block by zachmargolis 7c73eea3b138544fce8a

Cumulative Retention Rates

Full Screen

Retention

Interactive example on how retention can be cumulative. Based on @nmoryl‘s Built to Last: A Primer on Cohort Analysis.

Lower retention rates plateau early, higher retention rates quickly accumulate.

index.html

<!DOCTYPE html>
<html>
  <head>
    <style>
      .axis path,
      .axis line {
        fill: none;
        stroke: #555;
      }

      body {
        font-family: "Helvetica", sans-serif;
        font-weight: lighter;
      }
    </style>
  </head>
  <body>
    <form>
      <label for="retention">% Retention</label>
      <input name="retention" type="text" id="retention" />
      <input type="submit" value="update" />
    </form>

    <script src="d3.v3.min.js" type="text/javascript"></script>
    <script src="cohort.js" type="text/javascript"></script>
  </body>
</html>

Procfile

server: python -m SimpleHTTPServer

cohort.js

var margin = { left: 50, top: 10, right: 10, bottom: 30 },
    width = 960 - margin.left - margin.right,
    height = 500 - 50 - margin.top - margin.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 nMonths = 12,
    newPerMonth = 100;

var stack = d3.layout.stack()
  .values(function(d) { return d });

var data = buildCohortData(nMonths, newPerMonth, retention(retention() || 50));
var stacked = stack(data);

var x = d3.time.scale()
  .domain(d3.extent(stacked[0], function(d) { return d.x; }))
  .range([0, width]);

var y = d3.scale.linear()
  .domain([0, yMax()])
  .range([height, 0]);

var layers = svg.append('g').attr('class', 'layer');

svg.append('g').attr('class', 'y axis');

var xAxis = d3.svg.axis()
  .scale(x)
  .orient('bottom');

svg.append('g').attr('class', 'x axis')
  .attr('transform', 'translate(0, ' + height + ')')
  .call(xAxis)

var yAxis = d3.svg.axis()
  .scale(y)
  .orient('left');

svg.select('g.y.axis')
  .transition().duration(400)
  .call(yAxis);


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

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

var color = d3.scale.linear()
  .domain([0, nMonths])
  .range(['#339', '#99c']);

var areas = bindLayers(stacked);

areas.enter()
  .append('path')
  .attr('class', 'area')
  .attr('d', zeroArea)
  .attr('fill', function(d, i) { return color(i) })
  .attr('stroke', function(d, i) { return color(i) })

function update() {
  stacked = stack(buildCohortData(nMonths, newPerMonth, retention()));

  y.domain([0, yMax()])

  svg.select('g.y.axis')
    .transition().duration(400)
    .call(yAxis);

  areas.transition().duration(400)
    .attr('d', area)

  areas = bindLayers(stacked);

  areas.transition().delay(600).duration(400)
    .attr('d', area)
}

update();

function bindLayers(data) {
  return layers.selectAll('path.area').data(data, function(d, i) { return i; });
};

d3.select('form').on('submit', function() {
  d3.event.preventDefault();
  update();
  return false;
});

// set or get retention from the form
// always returns the current value
function retention(set) {
  var input = d3.select('#retention');

  if (arguments.length) {
    input.property('value', set);
    return set;
  } else {
    return parseFloat(input.property('value'));
  }
}

function yMax() {
  return d3.max(stacked, function(layer) {
    return d3.max(layer, function(d) { return d.y0 + d.y; });
  });
}

function buildCohortData(nMonths, newPerMonth, retentionPercent) {
  var retention = retentionPercent / 100;
  var data = [];
  for (var cohort = 0; cohort < nMonths; cohort++) {
    var values = [];

    data.push(values);

    for (var month = 0; month < nMonths; month++) {
      var value = 0;
      if (month >= cohort) {
        value = newPerMonth * Math.pow(retention, month - cohort);
      }

      values.push({
        x: new Date(Date.parse("2014-01-01").valueOf() + (1000 * 60 * 60 * 24 * 30 * month)),
        y: value
      });
    }
  }
  return data;
}