blob: 0d66aa84e48e6be97f860022405a1b8975446c3a [file] [log] [blame]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WiFi packets as seen from the space</title>
</head>
<body>
<script type='text/javascript'
src='//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'></script>
<script type='text/javascript' src='http://d3js.org/d3.v3.min.js'></script>
<script type='text/javascript'>
// TODO(katepek): Move into a separate JS file
var debug = true;
function log(o) {
if (debug) {
console.log(o);
}
}
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0];
var total_width = w.innerWidth || e.clientWidth || g.clientWidth;
var total_height = w.innerHeight || e.clientHeight || g.clientHeight;
var width; // of a plot
var height; // of a plot
var padding = 20;
// TODO(katepek): Pull in some standard library for this
var stream_colours =
['black', 'red', 'blue', 'green', 'magenta',
'gray', 'hotpink', 'chocolate', 'deepskyblue', 'gold'];
var field_settings = {
'pcap_secs': {
'parser': parseFloat,
'scale_type': d3.scale.linear(),
},
'seq': {
'parser': Number,
'scale_type': d3.scale.linear(),
},
'rate': {
'parser': Number,
'scale_type': d3.scale.log(),
},
'default': {
'parser': parseFloat,
'scale_type': d3.scale.linear(),
}
}
function settings(field) {
if (field_settings.hasOwnProperty(field))
return field_settings[field];
return field_settings['default'];
}
findIdxByPcapSecs = d3.bisector(raw('pcap_secs')).left;
var to_plot = []; // fields to be plotted against X axis (time)
var scales = {}; // dict[field; scale], incl. X axis
var reticle = {}; // dict[field; crosshair]
var dataset; // all packets
var streams; // pairs of (transmitter, receiver)
try {
$.getJSON('/json/' + get_query_param('key')[0], function(json) {
begin = new Date().getTime();
init(JSON.stringify(json));
for (idx in to_plot) {
visualize(to_plot[idx]);
}
end = new Date().getTime();
log('Spent on visualization ' + ((end - begin) / 1000) + ' sec.');
});
} catch (error) {
console.log(error);
}
function get_key() {
parts = window.location.pathname.split('/');
return parts[parts.length - 1];
}
function get_query_param(param) {
params = window.location.search.substring(1).split('&');
for (idx in params) {
if (params[idx].indexOf(param + '=') == 0)
return params[idx].split('=')[1].split(',');
}
return [];
}
function init(json_string) {
// TODO(katepek): Should sanitize here? E.g., discard bad packets?
// Packets w/o seq?
js_objects = JSON.parse(json_string);
dataset = JSON.parse(js_objects['js_packets']);
streams = JSON.parse(js_objects['js_streams']);
to_plot = get_query_param('to_plot');
// Leave only packets that have all the fields that we want to plot
// and the values there are positive
filter_dataset();
// Descending
dataset = dataset.sort(function(x, y) {
return raw('pcap_secs')(x) - raw('pcap_secs')(y);
});
height = (total_height - 4 * padding) / to_plot.length;
width = total_width - 4 * padding;
var x_range = [2 * padding, width - 2 * padding];
var y_range = [height - padding, padding];
log('total_height = ' + total_height);
log('height = ' + height);
add_scale('pcap_secs', x_range);
for (idx in to_plot) {
add_scale(to_plot[idx], y_range);
}
}
function filter_dataset() {
log('Before filtering: ' + dataset.length);
dataset = dataset.filter(function(d) {
if (!d.hasOwnProperty('pcap_secs')) return false;
if (raw('pcap_secs')(d) <= 0) return false;
for (idx in to_plot) {
if (!d.hasOwnProperty(to_plot[idx])) return false;
if (raw(to_plot[idx])(d) <= 0) return false;
}
return true;
});
log('After filtering: ' + dataset.length);
}
function add_scale(field, range) {
scales[field] = settings(field)['scale_type']
.domain([d3.min(dataset, raw(field)),
d3.max(dataset, raw(field))])
.range(range);
}
function raw(name) {
return function(d) {
return settings(name)['parser'](d[name]);
}
}
function scaled(name) {
return function(d) {
return scales[name](settings(name)['parser'](d[name]));
}
}
function visualize(field) {
log('About to visualize ' + field);
var svg = d3
.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('border', '1px solid black');
var focus = svg.append('g') .style('display', null);
reticle[field] = focus;
// TODO(katepek): Show a summary somewhere as a legend
// which pair corresponds to which colour
for (i = 0; i < streams.length; i++) {
log('pcap_vs_' + field + '_' + i + ' (' + get_colour(i) + ')');
svg
.selectAll('pcap_vs_' + field + '_' + i)
.data(dataset)
.enter()
.append('circle')
.filter(function(d) {
return d['ta'] == streams[i]['ta'] &&
d['ra'] == streams[i]['ra'];
})
.attr('cx', scaled('pcap_secs'))
.attr('cy', scaled(field))
.attr('r', 2)
.attr('fill', get_colour(i));
}
// TODO(katepek): Axes seem to show range, not the domain
var pcapSecsAxis = d3.svg.axis()
.scale(scales['pcap_secs'])
.orient('bottom')
.ticks(5);
var yAxis = d3.svg.axis()
.scale(scales[field])
.orient('right')
.ticks(5);
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + (height - padding) + ')')
.call(pcapSecsAxis);
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + (width - 2 * padding) + ',0)')
.call(yAxis);
// Add crosshairs
focus.append('line')
.attr('class', 'x')
.style('stroke', 'black')
.style('stroke-width', '3')
.style('stroke-dasharray', '13,13')
.style('opacity', 0.5)
.attr('y1', 0)
.attr('y2', height);
focus.append('line')
.attr('class', 'y')
.style('stroke', 'blue')
.style('stroke-width', '3')
.style('stroke-dasharray', '13,13')
.style('opacity', 0.5)
.attr('x1', 0)
.attr('x2', width);
focus.append('circle')
.attr('class', 'y')
.style('fill', 'none')
.style('stroke', 'blue')
.style('stroke-width', '3')
.attr('r', 4);
focus.append('text')
.attr('class', 'y1')
.attr('font-family', 'sans-serif')
.attr('font-size', '20px')
.attr('fill', 'black')
.attr('dx', 8)
.attr('dy', '-.5em');
// append the rectangle to capture mouse movements
svg.append('rect')
.attr('width', width)
.attr('height', height)
.style('fill', 'none')
.style('pointer-events', 'all')
.on('mouseover', function() {
for (i in Object.keys(reticle)) {
current = reticle[Object.keys(reticle)[i]];
current.style('display', null);
current.select('.y').style('display', null);
current.select('circle.y').style('display', null);
current.select('text.y1').style('display', null);
}
})
.on('mouseout', function() {
x = d3.mouse(this)[0];
if (x < scales['pcap_secs'].range()[0] ||
x > scales['pcap_secs'].range()[1]) {
for (i in Object.keys(reticle)) {
reticle[Object.keys(reticle)[i]].style('display', 'none');
}
}
})
.on('mousemove', function() {
var x = d3.mouse(this)[0],
y = d3.mouse(this)[1];
if (x < scales['pcap_secs'].range()[0] ||
x > scales['pcap_secs'].range()[1] ||
y > total_height)
return;
pcap_secs = scales['pcap_secs'].invert(x);
idx = findIdxByPcapSecs(dataset, pcap_secs, 1);
d0 = dataset[idx - 1];
d1 = dataset[idx];
d = Math.abs(x - scaled('pcap_secs')(d0)) >
Math.abs(x - scaled('pcap_secs')(d1)) ?
d1 : d0;
for (i in Object.keys(reticle)) {
r_field = Object.keys(reticle)[i];
closest_x = scaled('pcap_secs')(d);
closest_y = scaled(r_field)(d);
reticle[r_field].select('.x')
.attr('transform', 'translate(' + closest_x + ',0)');
reticle[r_field].select('.y')
.attr('transform', 'translate(0,' + closest_y + ')');
reticle[r_field].select('circle.y')
.attr('transform',
'translate(' + closest_x + ',' + closest_y + ')');
reticle[r_field].select('text.y1')
.attr('transform',
'translate(' + closest_x + ',' + closest_y + ')')
.text('secs=' + d['pcap_secs'] +
'; ' + r_field + '=' + d[r_field]);
}
});
}
function get_colour(i) {
if (i < stream_colours.length)
return stream_colours[i];
return stream_colours[i % stream_colours.length];
}
</script>
</body>
</html>