Created
August 23, 2012 07:59
-
-
Save ESeufert/3434053 to your computer and use it in GitHub Desktop.
A D3.js dashboard with a console for selecting dimensions
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> | |
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"> | |
<head> | |
<title>D3.js Dashboard Introduction</title> | |
<script src="http://d3js.org/d3.v2.js"></script> | |
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> | |
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.21/jquery-ui.min.js"></script> | |
<script> | |
function getMaxObjectValue(metric, graph_metric) { | |
var values = []; | |
for (var i = 0; i < metric.length; i++) { | |
for (var k = 0; k < graph_metric.length; k++) { | |
if (parseFloat(metric[i][""+graph_metric[k]]) < 1) { | |
values.push(parseFloat(metric[i][""+graph_metric[k]])); | |
} else { | |
values.push(Math.ceil(parseFloat(metric[i][""+graph_metric[k]]))); | |
} | |
} | |
} | |
values.sort(function(a,b){return a-b}); | |
return values[values.length-1]; | |
} | |
function getMinObjectValue(metric, graph_metric) { | |
var values = []; | |
for (var i = 0; i < metric.length; i++) { | |
for (var k = 0; k < graph_metric.length; k++) { | |
if (parseFloat(metric[i][""+graph_metric[k]]) < 1) { | |
values.push(parseFloat(metric[i][""+graph_metric[k]])); | |
} else { | |
values.push(Math.floor(parseFloat(metric[i][""+graph_metric[k]]))); | |
} | |
} | |
} | |
values.sort(function(a,b){return a-b}); | |
return values[0]; | |
} | |
function getDate(d) { | |
return new Date(d.date); | |
} | |
function buildLineChart(data, title, graph_metric, width, height, xaxislabel, yaxislabel) { | |
var metric = data.slice(0); | |
metric.splice(0,1); | |
// define graph size parameters | |
var margin = {top: 30, right: 10, bottom: 40, left: 60}, width = width - margin.left - margin.right, height = height - margin.top - margin.bottom; | |
//color scale for multiple lines | |
var colorscale = d3.scale.category10(); | |
var minDate = getDate(metric[0]), | |
maxDate = getDate(metric[metric.length-1]), | |
minObjectValue = getMinObjectValue(metric, graph_metric), | |
maxObjectValue = getMaxObjectValue(metric, graph_metric); | |
//create the graph object | |
var vis= d3.select("#metrics").append("svg") | |
.data(metric) | |
.attr("class", "metrics-container") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var y = d3.scale.linear().domain([ minObjectValue - (.1 * minObjectValue) , maxObjectValue + (.1 * maxObjectValue) ]).range([height, 0]), | |
x = d3.time.scale().domain([minDate, maxDate]).range([0, width]); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left") | |
.ticks(5); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom") | |
.ticks(2); | |
vis.append("text") | |
.attr("style", "font-family: Helvetica, Arial; font-size: 18px; font-weight: bold;") | |
.attr("dx", function(d) { return 10; }) | |
.attr("dy", function(d) { return -10; }) | |
.text(''+title); | |
//append the axes | |
vis.append("g") | |
.attr("class", "axis") | |
.call(yAxis); | |
vis.append("g") | |
.attr("class", "axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis); | |
//add the axes labels | |
vis.append("text") | |
.attr("class", "axis-label") | |
.attr("text-anchor", "end") | |
.attr("x", 20) | |
.attr("y", height + 34) | |
.text(xaxislabel); | |
vis.append("text") | |
.attr("class", "axis-label") | |
.attr("text-anchor", "end") | |
.attr("y", 6) | |
.attr("dy", "-5em") | |
.attr("transform", "rotate(-90)") | |
.text(yaxislabel); | |
var lines = []; | |
var circles = []; | |
//loop through graph objects, if it's a single graph there will only be one object | |
for (var z = 0; z < graph_metric.length; z++) { | |
lines[z] = d3.svg.line() | |
.x(function(d) { return x(getDate(d)); }) | |
.y(function(d) { return (isNaN( y(d[''+graph_metric[z]])) ? 0 : y(d[''+graph_metric[z]])); }) | |
vis.append("svg:path") | |
.attr("d", lines[z](metric)) | |
.style("stroke", function() { | |
return colorscale(""+z); | |
}) | |
.style("fill", "none") | |
.style("stroke-width", "2.5"); | |
var dataCirclesGroup = vis.append('svg:g'); | |
var circles = dataCirclesGroup.selectAll('.data-point') | |
.data(metric); | |
circles | |
.enter() | |
.append('svg:circle') | |
.attr('class', 'dot') | |
.attr('fill', function() { return colorscale(""+z); }) | |
.attr('cx', function(d) { return x(getDate(d)); }) | |
.attr('cy', function(d) { return (isNaN( y(d[''+graph_metric[z]])) ? 0 : y(d[''+graph_metric[z]])); }) | |
.attr('r', function() { return 3; }) | |
.on("mouseover", function(d) { | |
d3.select(this) | |
.attr("r", 8) | |
.attr("class", "dot-selected") | |
.transition() | |
.duration(750); | |
}) | |
.on("mouseout", function(d) { | |
d3.select(this) | |
.attr("r", 3) | |
.attr("class", "dot") | |
.transition() | |
.duration(750); | |
}) | |
.append("svg:title") | |
.text(function(d) { | |
var return_text = ""; | |
if (graph_metric.length > 1) { | |
return_text += graph_metric[z] + "\n"; | |
} | |
return_text += $.datepicker.formatDate('yy-mm-dd', new Date(d.date)) + ": "; | |
return_text += Math.round(d[''+graph_metric[z]] * 100) / 100; | |
return return_text; | |
}); | |
} | |
} | |
function safeDivide(num1, num2) { | |
if (isNaN(parseFloat(num2)) || isNaN(parseFloat(num1)) || num2 == 0) { | |
return 0; | |
} | |
return Math.round( parseFloat(num1) / parseFloat(num2) * 100 ) / 100; | |
} | |
function getData() { | |
var data = []; | |
var metrics = | |
{"countries": | |
[ | |
{ | |
"country": "USA", | |
"metrics": | |
[ | |
{ | |
"date" : "2012-08-19", | |
"DAU" : 500, | |
"DNU" : 200, | |
"sessions" : 100, | |
"sessions_length" : 2000, | |
"d1_retention" : 102, | |
"d7_retention" : 48, | |
"d30_retention" : 16 | |
}, | |
{ | |
"date" : "2012-08-20", | |
"DAU" : 800, | |
"DNU" : 300, | |
"sessions" : 120, | |
"sessions_length" : 4000, | |
"d1_retention" : 82, | |
"d7_retention" : 58, | |
"d30_retention" : 19 | |
}, | |
{ | |
"date" : "2012-08-21", | |
"DAU" : 1000, | |
"DNU" : 700, | |
"sessions" : 200, | |
"sessions_length" : 5000, | |
"d1_retention" : 285, | |
"d7_retention" : 126, | |
"d30_retention" : 9 | |
} | |
] | |
}, | |
{ | |
"country": "Estonia", | |
"metrics": | |
[ | |
{ | |
"date" : "2012-08-19", | |
"DAU" : 1500, | |
"DNU" : 1000, | |
"sessions" : 430, | |
"sessions_length" : 5100, | |
"d1_retention" : 948, | |
"d7_retention" : 698, | |
"d30_retention" : 294 | |
}, | |
{ | |
"date" : "2012-08-20", | |
"DAU" : 2094, | |
"DNU" : 1294, | |
"sessions" : 491, | |
"sessions_length" : 6958, | |
"d1_retention" : 1029, | |
"d7_retention" : 918, | |
"d30_retention" : 485 | |
}, | |
{ | |
"date" : "2012-08-21", | |
"DAU" : 2594, | |
"DNU" : 1592, | |
"sessions" : 592, | |
"sessions_length" : 8492, | |
"d1_retention" : 1349, | |
"d7_retention" : 1029, | |
"d30_retention" : 685 | |
} | |
] | |
}, | |
{ | |
"country": "Finland", | |
"metrics": | |
[ | |
{ | |
"date" : "2012-08-19", | |
"DAU" : 984, | |
"DNU" : 596, | |
"sessions" : 349, | |
"sessions_length" : 49852, | |
"d1_retention" : 294, | |
"d7_retention" : 102, | |
"d30_retention" : 55 | |
}, | |
{ | |
"date" : "2012-08-20", | |
"DAU" : 890, | |
"DNU" : 698, | |
"sessions" : 589, | |
"sessions_length" : 60921, | |
"d1_retention" : 304, | |
"d7_retention" : 198, | |
"d30_retention" : 78 | |
}, | |
{ | |
"date" : "2012-08-21", | |
"DAU" : 1201, | |
"DNU" : 509, | |
"sessions" : 492, | |
"sessions_length" : 70982, | |
"d1_retention" : 295, | |
"d7_retention" : 159, | |
"d30_retention" : 98 | |
} | |
] | |
} | |
] | |
}; | |
var i = 0; | |
$.each(metrics.countries, function() { | |
data[i] = []; | |
data[i][0] = this.country; | |
$.each(this.metrics, function() { | |
var metric = this; | |
var temp_date = new Date(this.date); | |
var month = temp_date.getMonth(); | |
var date = temp_date.getDate(); | |
var year = temp_date.getFullYear(); | |
metric.date = month + '/' + date + '/' + year; | |
data[i].push(metric); | |
}); | |
i++; | |
}); | |
return data; | |
} | |
function buildConsole(data) { | |
var html = ""; | |
html += "<div id=\"country-selector\" class=\"console-element\">"; | |
for (var i = 0; i < data.length; i++) { | |
html += "<div style=\"width: 150px; float: left;\">"; | |
html += "<input name=\"countries\" id=\"country-selector-" + data[i][0] + "\" type=\"checkbox\" "; | |
if (i == 0) { | |
html += "checked"; // set the first country to checked to provide some default data for the graphs | |
} | |
html += " value=\"" + data[i][0] + "\" /> "; | |
html += data[i][0]; | |
html += "</div>"; | |
} | |
html += "</div>"; | |
$('#console').html(html); | |
} | |
function createAverages(metric) { | |
for (var i = 1; i < metric.length; i++) { | |
metric[i].d1_retention_pct = safeDivide(metric[i].d1_retention, metric[i].DNU) * 100; | |
metric[i].d7_retention_pct = safeDivide(metric[i].d7_retention, metric[i].DNU) * 100; | |
metric[i].d30_retention_pct = safeDivide(metric[i].d30_retention, metric[i].DNU) * 100; | |
metric[i].average_session_length = safeDivide(metric[i].sessions_length, metric[i].sessions) / 60; | |
metric[i].average_sessions_per_user = safeDivide(metric[i].sessions, metric[i].DAU); | |
} | |
return metric; | |
} | |
function getCountries() { | |
var elements = $("input:checkbox[name=countries]:checked"); | |
var countries = []; | |
$.each(elements, function () { | |
countries.push($(this).val()); | |
}); | |
return countries; | |
} | |
function aggregateMetric(metrics, countries) { | |
var metric = []; //the single metric array of dates that we'll return, | |
//aggregated over the selected countries | |
var count = 0; // a count of countries being aggregated over | |
for (var i = 0; i < metrics.length; i++) { | |
if (jQuery.inArray(metrics[i][0], countries) > -1) { //this is a country we should aggregate for | |
if (count == 0) { | |
metric = cloneMetric(metrics[i]); // since metric is empty, set metric to the first set of metrics we find | |
count++; | |
} else { | |
metric[0] = metric[0] + metrics[i][0]; //combine the country names for auditing | |
for (var j = 1; j < metric.length; j++) { | |
//iterate through metric[j] object and add metrics[i][j] values to it. | |
//note: this requires that each country has the same number of days' worth of data! | |
for (var key in metrics[i][j]) { | |
if (key != "date") { // don't add the date | |
metric[j][""+key] += metrics[i][j][""+key]; | |
} | |
} | |
} | |
count++; | |
} | |
} | |
} | |
metric = createAverages(metric); //create the averaged values from the counts | |
//(like retention metrics, average session length, etc.) | |
return metric; | |
} | |
function resetCharts(metrics) { | |
var countries = getCountries(); // get checked items | |
var metric = aggregateMetric(metrics, countries); // build one metric item out of the selected countries and the full dataset | |
$('#metrics').html(''); // reset the metrics div html | |
buildLineChart(metric, "Retention", ['d1_retention_pct', 'd7_retention_pct', 'd30_retention_pct'], 1000, 300, "Date", "Retention (percentage)"); | |
buildLineChart(metric, "Daily New Users", ['DNU'], 500, 300, "Date", "New Users"); | |
buildLineChart(metric, "Daily Active Users", ['DAU'], 500, 300, "Date", "Users"); | |
buildLineChart(metric, "Average Session Length", ['average_session_length'], 500, 300, "Date", "Session Length (minutes)"); | |
buildLineChart(metric, "Average Sessions per User", ['average_sessions_per_user'], 500, 300, "Date", "Sessions per User"); | |
} | |
function cloneData(data) { | |
//create a "deep copy" of the objects within the metrics array | |
//ensures that objects are not copied by reference | |
var metrics = []; | |
for (var i = 0; i < data.length; i++) { | |
metrics.push(cloneMetric(data[i])); | |
} | |
return metrics; | |
} | |
function cloneMetric(metric) { | |
var tmp = []; | |
tmp[0] = metric[0]; | |
for (var k = 1; k < metric.length; k++) { | |
tmp.push( jQuery.extend(true, {}, metric[k]) ); | |
} | |
return tmp; | |
} | |
$(document).ready(function() { | |
var data = getData(); | |
buildConsole(data); | |
resetCharts(data); | |
$('#country-selector').change(function() { | |
var metrics = cloneData(data); | |
$('#country-selector').stop().css("background-color", "#FFFF9C").animate({ backgroundColor: "#CCFFE6"}, 1500); | |
resetCharts(metrics); | |
}); | |
}); | |
</script> | |
<style> | |
.axis path, .axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.line { | |
fill: none; | |
stroke-width: 1.5px; | |
} | |
body { | |
background: #FFFFFF; | |
background-repeat:repeat-x; | |
text-align: center; | |
font: 10px sans-serif; | |
} | |
.dot { | |
stroke-width: 1.5px; | |
} | |
.dot-selected { | |
fill: #B0C4DE; | |
stroke: #B0C4DE; | |
stroke-width: 1.5px; | |
} | |
.metrics-container { | |
width: auto; | |
height: auto; | |
padding: 10px 10px 10px 10px; | |
border-style: solid; | |
border-width: 1px; | |
float: left; | |
margin-left: 20px; | |
margin-top: 20px; | |
} | |
#console { | |
background-color: #CCFFE6; | |
font-size: 12px; | |
margin: 2px 2px 2px 2px; | |
padding: 4px 4px 4px 4px; | |
width: 95%; | |
float: left; | |
text-align: left; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>A D3.js Dashboard</h1> | |
<div id="console"> | |
</div> | |
<div id="metrics"> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment