Skip to content

Instantly share code, notes, and snippets.

@bstancil
Created August 5, 2015 23:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bstancil/71a49cad0e877b4ec096 to your computer and use it in GitHub Desktop.
Save bstancil/71a49cad0e877b4ec096 to your computer and use it in GitHub Desktop.
<link href="https://cdn.rawgit.com/jaz303/tipsy/master/src/stylesheets/tipsy.css" rel="stylesheet" type="text/css">
<style>
#wrapper {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
width: 900px;
margin: 0 auto;
padding-bottom: 10px;
}
.tipsy {
font-size: 14px;
line-height: 20px;
}
table { border-collapse: collapse; }
p {
text-align: center;
color: #666;
}
p.head {
text-align: center;
margin-bottom: 2px;
font-size: 16px;
text-transform: uppercase;
}
p.subhead {
text-align: center;
font-size: 14px;
color: #8B979C;
margin-bottom: 2px;
}
th {
background-color: #C3CBCB;
color: #fff;
font-weight: normal;
font-size: 10px;
line-height: 20px;
border-bottom: 2px solid white;
border-right: 2px solid white;
text-transform: uppercase;
}
td {
margin: 0px;
padding: 8px 5px;
width: 50px;
height: 14px;
font-size: 8px;
text-align: right;
}
hr {
height:1px;
border:none;
color:#ccc;
background-color:#ccc;
margin: 30px 0px;
}
td.major { cursor: pointer; }
td.footer { border-top: 2px Solid white; cursor: pointer; }
table.minor { margin-right: 10px; }
td.minor-cell {
width: 55px;
font-size: 10px;
padding-right: 20px;
}
.question {
cursor: pointer;
display: inline-block;
width: 16px;
height: 16px;
background-color: #89A4CC;
line-height: 16px;
color: White;
font-size: 12px;
border-radius: 8px;
text-align: center;
position: relative;
}
.question:hover { background-color: #3D6199; }
.tooltip {
background-color: #303030;
position: absolute;
right: 20px;
top: -100px;
z-index: 1000000;
width: 450px;
border-radius: 5px;
border: 2px solid black;
}
.tooltip p {
margin: 10px;
color: #E6E6E6;
padding: 10px;
font-weight: normal;
font-family: Helvetica, Arial, sans-serif;
font-size:13px;
text-align: left;
}
</style>
<div id="wrapper">
<p class="head">Table 1: Retention Percentage</p>
<p class="subhead">Periods after signup</p>
<div style="float:left" id='table-1-l'></div>
<div id='table-1'></div>
<hr>
<p class="head">Table 2: Churn Rate</p>
<p class="subhead">Periods after signup</p>
<div style="float:left" id='table-2-l'></div>
<div id='table-2'></div>
<hr>
<p class="head">Table 3: Churn Rate from Previous Period</p>
<p class="subhead">Periods after signup</p>
<div style="float:left" id='table-3-l'></div>
<div id='table-3'></div>
</div>
<script src="//cdn.rawgit.com/mbostock/d3/master/lib/colorbrewer/colorbrewer.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery.tipsy/1.0.2/jquery.tipsy.min.js"></script>
<script>
var f = d3.format(".0f");
var pp = d3.format(".1%");
var c = d3.format(",")
var formatDate = d3.time.format('%m-%d-%y');
var formatDateText = d3.time.format('%B %d, %y');
var r = d3.time.format("%Y-%m-%dT%H:%M:%S.000Z").parse;
function p(time) {
var t = new Date(Date.parse(time));
return r(t.toISOString());
}
var table1l = d3.select("#table-1-l").append("table").attr("class","minor");
var table2l = d3.select("#table-2-l").append("table").attr("class","minor");
var table3l = d3.select("#table-3-l").append("table").attr("class","minor");
var table1 = d3.select("#table-1").append("table");
var table2 = d3.select("#table-2").append("table");
var table3 = d3.select("#table-3").append("table");
data = dataset.content;
data.forEach(function(d) {
var maxAge = d3.max(_.pluck(_.where(data,{signup_date:d.signup_date}),"user_period"))
d["max_age"] = maxAge
})
var reds = ["#FFF6F4", "#FDE6DF", "#FBC8BB", "#FAA79A", "#C95966", "#EF646E", "#D54A62", "#BA435B", "#8F334F"],
blues = colorbrewer.Blues[9];
var signups = data.filter(function(d) { return d.user_period == 0; }),
totalSignups = d3.sum(_.pluck(signups,"retained_users"));
var dateOrder = d3.map();
signups.forEach(function(d,i) { dateOrder.set(d.signup_date, i); })
var tableData = getData(data);
drawLeftTable(table1l,signups);
drawLeftTable(table2l,signups);
drawLeftTable(table3l,signups);
drawTable(table1,tableData,"retained",blues)
drawTable(table2,tableData,"churn",reds)
drawTable(table3,tableData,"previousChurn",reds)
function drawTable(tableObj,fullDataObject,type,colors) {
var tableData = fullDataObject[0],
footerData = fullDataObject[1];
var headers = _.sortBy(
_.filter(
_.uniq(
_.pluck(data,"user_period")
),
function (d) { return d > 0; }),
function(d) { return d; });
var fillData = [];
signups.forEach(function(s,i) {
var row = [];
headers.forEach(function(h) {
row.push({date:i,column:h})
})
fillData.push(row)
})
var rangeMin = 1;
var rangeMax = 0;
var dates = [];
tableData.forEach(function(d) {
var localMax = d3.max(_.pluck(d,type));
var localMin = d3.min(_.pluck(d,type));
rangeMax = Math.max(rangeMax,localMax);
rangeMin = Math.min(rangeMin,localMin);
})
var cScale = d3.scale.quantize()
.domain([rangeMin,rangeMax])
.range(colors);
var textColor = ['#000000','#000000','#000000','#000000','#000000','#000000','#ffffff','#ffffff','#ffffff'];
var tScale = d3.scale.quantize()
.domain([rangeMin,rangeMax])
.range(textColor);
tableObj
.selectAll("th")
.data(headers)
.enter().append("th")
.text(function(d) { return d; });
tableObj
.selectAll("tr")
.data(fillData)
.enter().append("tr")
.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.attr("id",function(d) { return type + "-" + d.date + "-" + d.column;})
tableData.forEach(function(row,i) {
row.forEach(function(r) {
var rowNumber = dateOrder.get(r.date)
d3.selectAll("#" + type + "-" + rowNumber + "-" + r.period)
.text(function() { if (r.period != -1) { return pp(r[type]); } })
.attr("class",function() { if (r != null && r.period != -1) { return "major"; } })
.attr("title",function() { if (r != null && r.period != -1) { return hoverTextMajor(r,type); }})
.style("background",function() { if (r != null && r.period != -1) {return cScale(r[type]); }})
.style("color",function() { if (r != null && r.period != -1) { return tScale(r[type]); }});
})
})
tableObj
.selectAll("tfoot")
.data([1])
.enter().append("tr")
.selectAll("td")
.data(footerData)
.enter().append("td")
.text(function(d) { return pp(d[type]); })
.attr("class","footer")
.attr("title",function(d) { if (d != null) { return hoverTextMinor(d,type); }})
.style("background",function(d) { if (d != null) {return cScale(d[type]); }})
.style("color",function(d) { if (d != null) { return tScale(d[type]); }});
$('.major').tipsy({gravity:"se",opacity:1});
$('.footer').tipsy({gravity:"se",opacity:1});
}
function drawLeftTable(tableObj,data) {
var vals = [];
data.forEach(function(d) {
var obj = [p(d.signup_date), d.retained_users]
vals.push(obj);
})
vals = _.sortBy(vals, function(d){ return d[0]; });
tableObj
.selectAll("th")
.data(['Date','New Users'])
.enter().append("th")
.text(function(d) { return d; });
tableObj
.selectAll("tr")
.data(vals)
.enter().append("tr")
.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.attr("class","minor-cell")
.text(function(d,i) { if (i == 0) {
return formatDate(d);
} else {
return c(d);
}})
tableObj
.selectAll("tfoot")
.data([1])
.enter().append("tr")
.selectAll("td")
.data(["Total",c(totalSignups)])
.enter().append("td")
.attr("class","footer minor-cell")
.text(function(d) { return d; })
}
function getData(data) {
var headers = _.sortBy(_.uniq(_.pluck(data,"user_period")),function(d) { return d; });
var dates = _.sortBy(_.uniq(_.pluck(data,"signup_date")),function(d) { return p(d); });
var tableObject = [];
var footerObject = [];
dates.forEach(function(d,i) {
var row = [];
var dateFilter = _.where(data,{signup_date:d});
var signups = _.where(dateFilter,{user_period:0})[0].retained_users;
headers.forEach(function(h) {
// Calculate footer values
if (i == 0 && h > 0) {
var ageFilter = _.filter(data,function(d) { return d.max_age >= h; })
var allSignups = d3.sum(_.pluck(_.where(ageFilter,{user_period:0}),"retained_users"));
var allRetained = d3.sum(_.pluck(_.where(ageFilter,{user_period:h}),"retained_users"));
var allPrevious = d3.sum(_.pluck(_.where(ageFilter,{user_period:Math.max(h - 1,0)}),"retained_users"));
var footerCell = {
period: h,
retained: allRetained/allSignups,
churn: (allPrevious - allRetained)/allSignups,
previousChurn: (allPrevious - allRetained)/allPrevious
}
footerObject.push(footerCell)
}
// Calculate values for cell
var currentPeriod = _.where(dateFilter,{user_period:h});
var previousPeriod = _.where(dateFilter,{user_period:Math.max(h - 1,0)});
var maxAge = d3.max(_.pluck(dateFilter,"max_age"));
if (maxAge >= h && h > 0) {
var retained = 0,
previous = 0;
if (currentPeriod.length != 0) { retained = currentPeriod[0].retained_users; }
if (previousPeriod.length != 0) { previous = previousPeriod[0].retained_users; }
// Handle division by 0 for previous period churn
if (previous == 0) { pc = 0; } else { pc = (previous - retained)/previous; }
cell = {
date: d,
period: h,
signups:signups,
retained:retained/signups,
churn:(previous - retained)/signups,
previousChurn:pc
};
row.push(cell)
}
})
if (row.length > 0) { tableObject.push(row); }
})
return [tableObject,footerObject];
}
function hoverTextMajor(d,type) {
if (type == "retained") {
return pp(d[type]) +
" of users who signed up during the period of " +
formatDateText(p(d.date)) + " came back " +
d.period + " periods later. ";
} else if (type == "churn") {
return pp(d[type]) +
" of users who signed up during the period of " +
formatDateText(p(d.date)) + " churned " +
d.period + " periods later. ";
} else if (type == "previousChurn") {
return pp(d[type]) +
" of users who signed up the period of " +
formatDateText(p(d.date)) + " and retained for " +
(d.period - 1) + " periods churned in their " +
sfx(d.period) + " period.";
}
}
function hoverTextMinor(d,type) {
if (type == "retained") {
return pp(d[type]) +
" of users who signed up came back " +
d.period + " periods later. ";
} else if (type == "churn") {
return pp(d[type]) +
" of users who signed up churned " +
d.period + " periods later. ";
} else if (type == "previousChurn") {
return pp(d[type]) +
" of users who signed up and retained for " +
(d.period - 1) + " periods churned in their " +
sfx(d.period) + " period.";
}
}
function sfx(int) {
if (int == 11) { return "11th"; }
else if (int == 12) { return "12th"; }
else if (int == 13) { return "13th"; }
else if (int % 10 == 1) { return int + "st"; }
else if (int % 10 == 2) { return int + "nd"; }
else if (int % 10 == 3) { return int + "rd"; }
else { return int + "th"; }
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment