Created
May 6, 2014 16:45
-
-
Save ihodes/0be5ae4ffcf401eba183 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
<html> | |
<head> | |
<style> | |
.node circle { | |
fill: white; | |
stroke: steelblue; | |
stroke-width: 1.5px; | |
} | |
.node { | |
font: 15px sans-serif; | |
font-weight: 600; | |
fill: black; | |
} | |
.link { | |
fill: none; | |
stroke: #ccc; | |
stroke-width: 0.5px; | |
crispEdges: true; | |
} | |
#tooltip { | |
line-height: 1; | |
font-weight: bold; | |
padding: 9px; | |
background: rgba(0, 0, 0, 0.8); | |
color: #fff; | |
border-radius: 2px; | |
} | |
#rchart { | |
height: 300px; | |
float: left; | |
margin-left: 0; | |
margin-top: 0; | |
} | |
#rchart svg{ | |
height: 250px; | |
width:490px; | |
} | |
#vchart { | |
height: 300px; | |
} | |
#vchart svg{ | |
height: 250px; | |
width:490px; | |
} | |
#uchart svg { | |
height: 300px; | |
width:1000px; | |
} | |
#weekly { | |
height: 400px; | |
padding-top:50px; | |
padding-left:40px; | |
} | |
#weekly svg{ | |
height: 375px; | |
width:900px; | |
} | |
.set_values { | |
padding-left: 75px; | |
} | |
#container { | |
max-width: 1050px; | |
margin: 0 auto; | |
} | |
h1{ | |
text-align:center; | |
padding:25px; | |
} | |
h2 { | |
text-align:center; | |
padding-top:25px; | |
} | |
</style> | |
</head> | |
<body> | |
<link href="https://raw.githubusercontent.com/novus/nvd3/master/src/nv.d3.css" rel="stylesheet" type="text/css" /> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://nvd3.org/assets/js/nv.d3.js"></script> | |
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> | |
<script src="http://cdnjs.cloudflare.com/ajax/libs/mathjs/0.21.0/math.min.js"></script> | |
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script> | |
<div id="container"> | |
<h1> Retention/Virality Tradeoffs </h1> | |
<div id="rchart"> | |
<svg></svg> | |
<span class="set_values"> | |
<input type="text" id="rfunction" value="1 - e^(-0.5*x^2)-.01"><button type="button" id="rset">Set Retention</button><br> | |
</span> | |
</div> | |
<div id="vchart"> | |
<svg></svg> | |
<span class="set_values"> | |
<input type="text" id="vfunction" value="e^(-1.2*x^2)+.1"><button type="button" id="vset">Set Virality</button><br> | |
</span> | |
</div> | |
<div id="uchart"> | |
<svg></svg> | |
<span class="set_values"> | |
<input type="text" id="numusers" value="10000"><button type="button" id="uset">Set Users</button><br> | |
</span> | |
</div> | |
<div id="weekly"> | |
<svg></svg> | |
</div> | |
<h2>Retention/Virality Tree</h2> | |
<div id="viz"> | |
</div> | |
</div> | |
<script> | |
////////////////// | |
// Evil Globals // | |
////////////////// | |
var gVirality = "e^(-1.2*x^2)+.1" | |
var gRetention = "1 - e^(-0.5*x^2)-.01" | |
var root = null | |
var users = 10000 | |
var math = mathjs(); | |
/////////////////// | |
// My preciouses // | |
/////////////////// | |
function reduce(f, acc, lst) { | |
for (var i in lst) { | |
acc = f(acc, lst[i]); | |
} | |
return acc; | |
} | |
function map(f, lst) { | |
var result = []; | |
for (var i in lst) { | |
result.push(f(lst[i])); | |
} | |
return result; | |
} | |
function mapcat(f, lst) { | |
var result = []; | |
for (var i in lst) { | |
result = result.concat(f(lst[i])); | |
} | |
return result; | |
} | |
function filter(f, lst) { | |
return reduce(function(acc, n) { | |
if (f(n)) acc.push(n); | |
return acc; | |
}, [], lst); | |
} | |
//////////////////////// | |
/// Generate VR tree /// | |
//////////////////////// | |
function virality(level) { | |
virstr = gVirality.replace("x", level); | |
// console.log(virstr) | |
val = math.eval(virstr) | |
return val | |
} | |
function retention(level) { | |
retstr = gRetention.replace("x", level); | |
// console.log(retstr) | |
val = math.eval(retstr) | |
return val | |
} | |
var V = "V"; | |
var R = "R"; | |
var ROOT = "ROOT" | |
function Node(type, population, parent, level) { | |
var lineage = [].concat(parent.lineage); | |
lineage.push(parent); | |
return {type: type, level: level, population: population, | |
lineage: lineage, children: []}; | |
} | |
function spawn(node) { | |
var lvl = node.level; | |
if (node.type === V || node.type === ROOT) | |
return [Node(R, node.population * retention(1), node, 1), | |
Node(V, node.population * virality(1), node, 1)]; | |
else | |
return [Node(R, node.population * retention(lvl + 1), node, lvl + 1), | |
Node(V, node.population * virality(lvl + 1) , node, lvl + 1)]; | |
} | |
function treeDepth(tree) { | |
var nodes = tree.children; | |
var depth = 0; | |
while (nodes && nodes[0] && nodes[0].children) { | |
depth = depth + 1; | |
nodes = nodes[0].children; | |
} | |
return depth; | |
} | |
function childrenAtDepth(tree, depth) { | |
var d = 0; | |
var nodes = [tree]; | |
while (d < depth) { | |
nodes = mapcat(function(n){return n.children;}, nodes); | |
d = d + 1; | |
} | |
return nodes; | |
} | |
function deepestChildren(tree) { | |
return childrenAtDepth(tree, treeDepth(tree)); | |
} | |
function populationAtDepth(tree, depth) { | |
var nodes = childrenAtDepth(tree, depth); | |
return reduce(function(acc, n){ return acc + n.population; }, 0, nodes); | |
} | |
function populationAtDepthByType(tree, depth, type) { | |
var nodes = childrenAtDepth(tree, depth); | |
matchingNodes = filter(function(n){return n.type == type;}, nodes) | |
return reduce(function(acc, n){ return acc + n.population; }, 0, matchingNodes); | |
} | |
function generateTree(depth, initialPopulation, /* ignore: */ tree) { | |
if (tree === undefined) | |
return generateTree(depth, null, initializeTree(initialPopulation)); | |
else if (depth === 0) | |
return tree; | |
else | |
return generateTree(depth - 1, null, generateLevel(tree)); | |
} | |
function initializeTree(initialPopulation) { | |
return { type: ROOT, level: 0, | |
children: [], lineage: [], | |
population: initialPopulation }; | |
} | |
function generateLevel(tree) { | |
var children = deepestChildren(tree); | |
map(function(n){ return n.children = spawn(n); }, children); | |
return tree; | |
} | |
/////////////////////////////////////////////////// | |
/////////// Visualization!!!11!one! ////////// | |
/////////////////////////////////////////////////// | |
function drawGraph() { | |
var POP = users; | |
var data = generateTree(7, POP); | |
var margin = {top: 100, right: 50, bottom: 100, left: 50}; | |
var width = 900 - margin.left - margin.right, | |
height = 1200 - margin.top - margin.bottom; | |
var radius = 50; | |
function r(d) { | |
return Math.sqrt((d.population / POP) * radius * radius); | |
} | |
var tooltip = d3.tip() | |
.attr("id", "tooltip") | |
.offset([-10, 0]) | |
.html(function(d) { | |
var lin = reduce(function(str, n){ | |
return str + n.type + d.level + "→"; | |
}, "", d.lineage); | |
return "<strong style='color:steelblue'>Population:</strong><span style='color:white'> " + Math.round(d.population) + "</span>" + | |
"<br><span style='color:#d4d4d4;font-size:10px'>" + lin.substring(0, lin.length - 4) + "</span>"; // remove 4 bytes from the arrow | |
}) | |
var cluster = d3.layout.cluster() | |
.size([height, width - 160]); | |
var diagonal = d3.svg.diagonal() | |
.projection(function(d) { return [d.x/1.1, d.y]; }); | |
var svg = d3.select("#viz").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 + ")"); | |
svg.call(tooltip); | |
root = data; | |
var nodes = cluster.nodes(root), | |
links = cluster.links(nodes); | |
var link = svg.selectAll(".link") | |
.data(links) | |
.enter().append("path") | |
.attr("class", "link") | |
.attr("d", diagonal); | |
var node = svg.selectAll(".node") | |
.data(nodes) | |
.enter().append("g") | |
.attr("class", "node") | |
.attr("transform", function(d) { return "translate(" + d.x/1.1 + "," + d.y + ")"; }) | |
.on("mouseover", tooltip.show) | |
.on("mouseout", tooltip.hide); | |
node.append("circle") | |
.attr("r", r) | |
.style("stroke", function(d) { return getColor(d.type) }); | |
node.append("text") | |
.attr("dx", function(d) { return d.children ? -5 - r(d) : 10; }) | |
.attr("dy", 5) | |
.style("font-size", function (d) {return 10/(d.lineage.length/3)}) | |
.style("text-anchor", function(d) { return d.children ? "end" : "start"; }) | |
.text(function(d) { return d.type === ROOT ? "" | |
: d.type + String(d.level); }); | |
d3.select(self.frameElement).style("height", height + "px"); | |
}; | |
////////////////////////// | |
/// Draw Pretty Charts /// | |
////////////////////////// | |
function drawRetention() { | |
nv.addGraph(function() { | |
var rchart = nv.models.lineChart() | |
.forceY([0,1]) | |
.margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room. | |
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline! | |
.transitionDuration(350) //how fast do you want the lines to transition? | |
.showLegend(true) //Show the legend, allowing users to turn on/off line series. | |
.showYAxis(true) //Show the y-axis | |
.showXAxis(true) //Show the x-axis | |
; | |
rchart.xAxis //Chart x-axis settings | |
.axisLabel('Time (weeks)') | |
.tickFormat(d3.format(',r')); | |
rchart.yAxis //Chart y-axis settings | |
.axisLabel('Retention %') | |
.tickFormat(d3.format('%')); | |
/* Done setting the chart up? Time to render it!*/ | |
var rData = retentionData(); //You need data... | |
d3.select('#rchart svg') //Select the <svg> element you want to render the chart in. | |
.datum(rData) //Populate the <svg> element with chart data... | |
.call(rchart); //Finally, render the chart! | |
//Update the chart when window resizes. | |
nv.utils.windowResize(function() { rchart.update() }); | |
return rchart; | |
}); | |
} | |
function drawVirality() { | |
nv.addGraph(function() { | |
var vchart = nv.models.lineChart() | |
.forceY([0,1]) | |
.margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room. | |
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline! | |
.transitionDuration(350) //how fast do you want the lines to transition? | |
.showLegend(true) //Show the legend, allowing users to turn on/off line series. | |
.showYAxis(true) //Show the y-axis | |
.showXAxis(true) //Show the x-axis | |
; | |
vchart.xAxis //Chart x-axis settings | |
.axisLabel('Time (weeks)') | |
.tickFormat(d3.format(',r')); | |
vchart.yAxis //Chart y-axis settings | |
.axisLabel('Virality') | |
.tickFormat(d3.format('.02f')); | |
var vData = viralityData(); //You need data... | |
d3.select('#vchart svg') //Select the <svg> element you want to render the chart in. | |
.datum(vData) //Populate the <svg> element with chart data... | |
.call(vchart); //Finally, render the chart! | |
//Update the chart when window resizes. | |
nv.utils.windowResize(function() { vchart.update() }); | |
return vchart; | |
}); | |
} | |
function drawUsers() { | |
nv.addGraph(function() { | |
var uchart = nv.models.lineChart() | |
.margin({left: 100}) //Adjust chart margins to give the x-axis some breathing room. | |
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline! | |
.transitionDuration(350) //how fast do you want the lines to transition? | |
.showLegend(true) //Show the legend, allowing users to turn on/off line series. | |
.showYAxis(true) //Show the y-axis | |
.showXAxis(true) //Show the x-axis | |
; | |
uchart.xAxis //Chart x-axis settings | |
.axisLabel('Time (weeks)') | |
.tickFormat(d3.format(',r')); | |
uchart.yAxis //Chart y-axis settings | |
.axisLabel('Users') | |
.tickFormat(d3.format('.0f')); | |
/* Done setting the chart up? Time to render it!*/ | |
var uData = userData(); //You need data... | |
d3.select('#uchart svg') //Select the <svg> element you want to render the chart in. | |
.datum(uData) //Populate the <svg> element with chart data... | |
.call(uchart); //Finally, render the chart! | |
//Update the chart when window resizes. | |
nv.utils.windowResize(function() { uchart.update() }); | |
return uchart; | |
}); | |
} | |
function drawWeekly() { | |
nv.addGraph(function() { | |
var chart = nv.models.multiBarChart() | |
.transitionDuration(350) | |
.reduceXTicks(true) //If 'false', every single x-axis tick label will be rendered. | |
.rotateLabels(0) //Angle to rotate x-axis labels. | |
.showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. | |
.groupSpacing(0.1) //Distance between each group of bars. | |
; | |
chart.xAxis | |
.tickFormat(d3.format(',f')); | |
chart.yAxis | |
.tickFormat(d3.format('.0f')); | |
d3.select('#weekly svg') | |
.datum(weeklyData()) | |
.call(chart); | |
nv.utils.windowResize(chart.update); | |
return chart; | |
}); | |
}; | |
/////////////////////////// | |
/// Get Data For Charts /// | |
/////////////////////////// | |
function retentionData() { | |
var rdata = [] | |
//Data is represented as an array of {x,y} pairs. | |
for (var i = 1; i <= treeDepth(root)+1; i++) { | |
ret = retention(i) | |
rdata.push({x: i, y: ret}); | |
} | |
//Line chart data should be sent as an array of series objects. | |
return [ | |
{ | |
values: rdata, //values - represents the array of {x,y} data points | |
key: 'Retention', //key - the name of the series. | |
color: '#003399' //color - optional: choose your own line color. | |
} | |
]; | |
} | |
function viralityData() { | |
var vdata = [] | |
//Data is represented as an array of {x,y} pairs. | |
for (var i = 1; i <= treeDepth(root)+1; i++) { | |
vir = virality(i) | |
vdata.push({x: i, y: vir}); | |
} | |
//Line chart data should be sent as an array of series objects. | |
return [ | |
{ | |
values: vdata, //values - represents the array of {x,y} data points | |
key: 'Virality', //key - the name of the series. | |
color: '#cc0000' //color - optional: choose your own line color. | |
} | |
]; | |
} | |
function userData() { | |
var udata = [] | |
//Data is represented as an array of {x,y} pairs. | |
for (var i = 0; i <= treeDepth(root)+1; i++) { | |
udata.push({x: i, y: populationAtDepth(root,i)}); | |
} | |
//Line chart data should be sent as an array of series objects. | |
return [ | |
{ | |
values: udata, //values - represents the array of {x,y} data points | |
key: 'Users', //key - the name of the series. | |
color: '#000000' //color - optional: choose your own line color. | |
} | |
]; | |
} | |
function weeklyData() { | |
var rdata = [] | |
var vdata = [] | |
//Data is represented as an array of {x,y} pairs. | |
for (var i = 1; i <= treeDepth(root)+1; i++) { | |
rdata.push({x: i, y: populationAtDepthByType(root,i,R)}); | |
vdata.push({x: i, y: populationAtDepthByType(root,i,V)}); | |
} | |
//Line chart data should be sent as an array of series objects. | |
return [ | |
{ | |
values: rdata, //values - represents the array of {x,y} data points | |
key: 'Retention', //key - the name of the series. | |
color: '#003399' //color - optional: choose your own line color. | |
}, | |
{ | |
values: vdata, //values - represents the array of {x,y} data points | |
key: 'Virality', //key - the name of the series. | |
color: '#cc0000' //color - optional: choose your own line color. | |
} | |
]; | |
} | |
//////////////////////////// | |
/// Formatting Functions /// | |
//////////////////////////// | |
function getColor(type) { | |
if (type == "V") { | |
return "#cc0000" | |
} | |
else if (type == "R") { | |
return "#003399" | |
} | |
else { | |
return "#000000" | |
} | |
} | |
///////////// | |
/// Setup /// | |
///////////// | |
$( "#rset" ).click(function() { | |
gRetention = $( "#rfunction" ).val() | |
drawRetention() | |
$( "#viz" ).empty(); | |
drawGraph(); | |
drawUsers(); | |
}); | |
$( "#vset" ).click(function() { | |
gVirality = $( "#vfunction" ).val() | |
drawVirality() | |
$( "#viz" ).empty(); | |
drawGraph(); | |
drawUsers(); | |
}); | |
$( "#uset" ).click(function() { | |
users = parseInt($( "#numusers" ).val()) | |
$( "#viz" ).empty(); | |
drawGraph(); | |
drawUsers(); | |
}); | |
drawRetention(); | |
drawVirality(); | |
$( "#viz" ).empty(); | |
drawGraph(); | |
drawUsers(); | |
drawWeekly(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment