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.
<!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>
server: python -m SimpleHTTPServer
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;
}