Skip to content

Instantly share code, notes, and snippets.

@ihodes
Created May 6, 2014 16:45
Show Gist options
  • Save ihodes/0be5ae4ffcf401eba183 to your computer and use it in GitHub Desktop.
Save ihodes/0be5ae4ffcf401eba183 to your computer and use it in GitHub Desktop.
<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 + "&#8594;";
}, "", 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