A D3.js dashboard with a console for selecting dimensions
<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