Created
August 5, 2015 23:40
-
-
Save bstancil/71a49cad0e877b4ec096 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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