Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@ESeufert
Created August 23, 2012 07:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ESeufert/3434053 to your computer and use it in GitHub Desktop.
Save ESeufert/3434053 to your computer and use it in GitHub Desktop.
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