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; }