Skip to content

Instantly share code, notes, and snippets.

@jamesonthecrow
Last active December 26, 2016 08:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jamesonthecrow/2124496db9a9bd8d3e40 to your computer and use it in GitHub Desktop.
Save jamesonthecrow/2124496db9a9bd8d3e40 to your computer and use it in GitHub Desktop.
A visualization to explore timeseries of MIT wifi network data.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MIT Wifi - Timeseries</title>
<link rel="stylesheet" href="styles.css">
<style>
#banner {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 320px;
background-color: white;
}
#instructions {
width: 170px;
position: fixed;
left: 0;
top: 0;
}
#single-graph .access-point .line {
fill: none;
stroke-width: 1.5px;
opacity: 1.0;
}
#single-graph {
width: 1000px;
height: 310px;
padding-top:10px;
margin: auto;
}
#single-graph-ui {
float: right;
padding-right: 10px;
text-decoration: none;
}
#small-multiples {
width: 1000px;
margin: 330px auto;
-webkit-column-width: 100px;
-webkit-column-gap: 0px;
-webkit-column-rule: none;
}
#small-multiples svg:hover, #small-multiples .selected {
background-color: #EEEEEE;
}
#small-multiples .access-point .line {
fill: none;
stroke-width: 1.5px;
opacity: 1.0;
}
#small-multiples ul {
list-style-type: none;
padding: 0;
}
#small-multiples ul li {
float: right;
padding: 0;
}
#small-multiples ul li .ap-id-text{
opacity: 0.25;
font-family: sans-serif;
font-size: 1.25em;
}
#small-multiples ul li .ap-id-text:hover{
opacity: 0.75;
}
body {
font: 10px sans-serif;
}
.axis path {
display: none;
}
.axis line {
shape-rendering: crispEdges;
stroke: #333;
}
.axis .minor line {
stroke: #CCC;
stroke-dasharray: 2,2;
}
</style>
</head>
<body>
<div id="banner">
<div id="instructions">
<ol><label>Instructions:</label>
<li>Scroll through timerseries of unique devices connected to access points around campus.</li>
<li>Select/de-select a time series by clicking on it.</li>
<li>Access points for the same building are shown in the same color.</li>
</ol>
</div>
<div id="single-graph">
<div id="single-graph-ui">
<a href="#" onclick="UnselectAll()">clear</a>
</div>
</div>
</div>
<div id="small-multiples">
<ul id="small-multiples-list"></ul>
</div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
function SingleGraph() {
this.margin = {top: 30, right: 20, bottom: 20, left: 20};
this.width = 1000 - this.margin.right - this.margin.left;
this.height = 300 - this.margin.top - this.margin.bottom;
this.x = d3.time.scale().
range([0, this.width]).
domain([new Date('9/16/2014'), new Date('9/20/2014')]);
this.y = d3.scale.linear().
range([this.height, 0]).
domain([0, 20]);
this.xAxis = d3.svg.axis()
.scale(this.x)
.ticks(d3.time.hours,6)
.tickSize(-this.height)
.orient("bottom");
this.yAxis = d3.svg.axis()
.scale(this.y)
.tickSize(this.width)
.orient("right");
};
SingleGraph.prototype.Initialize = function() {
var svg = d3.select("#single-graph").append("svg").
attr("width", this.width + this.margin.left + this.margin.right).
attr("height", this.height + this.margin.top + this.margin.bottom)
.append("g")
.attr("class", "graph")
.attr("transform", "translate(" + this.margin.left + "," +
this.margin.top + ")");
var gx = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this.height + ")")
.call(this.xAxis)
gx.selectAll("g").filter(function(d) { return d; })
.classed("minor", true);
var gy = svg.append("g")
.attr("class", "y axis")
.call(this.yAxis);
gy.selectAll("g").filter(function(d) { return d; })
.classed("minor", true);
gy.selectAll("text")
.attr("x", 4)
.attr("dy", -4);
gy.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -10)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Unique Devices");
};
SingleGraph.prototype.Clear = function() {
d3.select("#single-graph svg .graph").
selectAll(".access-point").
data([]).exit().remove()
};
SingleGraph.prototype.Update = function(selected_data) {
this.Clear();
selected_data.series = d3.selectAll('.selected')[0].map(function(d) {
return d.__data__;});
if (selected_data.series.length <= 0) {return true;};
var y = this.y.domain([0, d3.max(selected_data.series.map(function(d) {
return d3.max(d.timeseries);})
)]);
var x = this.x;
// Update axis
var yaxis = d3.select('#single-graph svg').select('.y.axis')
yaxis.call(this.yAxis);
yaxis.selectAll("g").filter(function(d) { return d; })
.classed("minor", true);
yaxis.selectAll(".tick text")
.attr("x", 4)
.attr("dy", -4);
var dateArray = d3.time.scale()
.domain([new Date('9/16/2014'), new Date('9/20/2014')])
.ticks(d3.time.minutes, 15)
var line = d3.svg.line().
interpolate("basis").
x(function(d, i) {return x(dateArray[i]);}).
y(function(d) {return y(d);});
var access_points = d3.select("#single-graph svg .graph").
selectAll(".access-point").
data(selected_data.series).
enter().
append("g").
attr("class", "access-point");
// Plot the timeseries
access_points.append("path").
attr("class", "line").
attr("d", function(d, i) {return line(d.timeseries, i);}).
attr("stroke", function(d) {return building_colors[d.building].toString();});
// Add a label.
access_points.append("text").
attr("class", "ap-label").
attr("x", function(d) {return 900;}).
attr("y", function(d,i) {return i*12;}).
attr("text-anchor", "start").
attr("fill", function(d) {return building_colors[d.building].toString();}).
text(function(d) { return d.ap_id.toUpperCase(); });
};
SingleGraph.prototype.UnselectAll = function() {
d3.selectAll(".selected").classed("selected",false);
this.Clear();
selected_data = {'series': []};
};
var raw_data;
var timeseries_len;
var selected_data = {};
var building_colors = {};
var selected_idx = d3.range(4).map(function() {return d3.round(Math.random()*100);});
var singleGraph = new SingleGraph();
singleGraph.Initialize();
d3.json("https://dl.dropboxusercontent.com/u/4035638/clean_timeseries_4day.json", function(error, json) {
if (error) return console.warn(error);
raw_data = json;
timeseries_len = raw_data.series[0].timeseries.length;
populate_building_colors(raw_data);
small_multiples(raw_data);
selected_data.series = raw_data.series.filter(
function(d,i) {return selected_idx.indexOf(i) >= 0;});
singleGraph.Update(selected_data);
});
function small_multiples(raw_data) {
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var width = 100 - margin.right - margin.left;
var height = 50 - margin.top - margin.bottom;
// Generate random timeseries data.
var timeseries_len = 24*4;
var x = d3.time.scale().
range([0, width]).
domain([0, raw_data.series[0].timeseries.length]);
var y = d3.scale.linear().
range([height, 0]).
domain([0, 100]);
var line = d3.svg.line().
interpolate("basis").
x(function(d, i) {return x(i);}).
y(function(d) {return y(d);});
// Sort series by building number.
raw_data.series.sort(function(a, b) {
if (a.ap_id < b.ap_id) return -1;
else return 1;
})
// Plot timeseries.
var access_point = d3.select("#small-multiples ul").selectAll("svg").
data(raw_data.series.slice(0,1000)).
enter().append("li").append("svg").
attr("width", width + margin.left + margin.right).
attr("height", height + margin.top + margin.bottom).
on("click", function(d) {
var clicked_data = this.__data__;
if (!d3.select(this).classed("selected")) {
d3.select(this).classed("selected", true);
} else {
selected_data.series = selected_data.series.
filter(function(d) {
return d.ap_id != clicked_data.ap_id;
});
d3.select(this).classed("selected", false);
}
singleGraph.Update(selected_data);
}).
append("g").
attr("class", "access-point").
attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Add the spark line path.
access_point.append("path")
.attr("class", "line")
.attr("d", function(d, i) {
y.domain([0, d3.max(d.timeseries)]);
return line(d.timeseries, i); }).
attr("stroke", function(d) {return building_colors[d.building].toString();});
// Add some text
access_point.append("text").
attr("class","ap-id-text").
attr("x", width/2).
attr("y", (height + margin.top)/2).
attr("text-anchor", "middle").
text(function(d) { return (d.building+"-"+d.room).toUpperCase(); });
};
function generate_random_data(ntimeseries, len) {
/*
* Generates random timeseries data.
*/
var data = d3.range(ntimeseries).map(function(d, i) { return {
'idx': i,
'building': Math.random() * 10,
'room': Math.random() * 300,
'ap_id': 'lorem-123',
'timeseries': generate_timeseries(len)
}
});
return data;
};
function generate_timeseries(len) {
/*
* Generates a single random timeseries.
*/
return d3.range(len).
map(function() {return Math.floor(Math.random()*100);});
};
function populate_building_colors(raw_data) {
/*
* Assigns a unique, random color to each building
*/
var buildings = d3.set(raw_data.series.map(function(d) {
return d.building; })).values();
for(var i=0, building; building=buildings[i]; i++){
building_colors[building] = d3.rgb(d3.round(Math.random()*255),
d3.round(Math.random()*255),
d3.round(Math.random()*255))
}
};
function UnselectAll() {
console.log(singleGraph);
singleGraph.UnselectAll();
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment