-
-
Save NormanEdance/4013b8a72fe97ea6f3522114158a8736 to your computer and use it in GitHub Desktop.
Streaming data to cubism.js with websockets
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
<!DOCTYPE html> | |
<meta charset='utf-8'> | |
<head> | |
<title>Cubism + Websockets</title> | |
<script language='javascript' src='d3.min.js'></script> | |
<script language='javascript' src='cubism.v1.js'></script> | |
<script language='javascript'> | |
/* I can never seem to remember: | |
Array.push() appends to the end, and returns the new length | |
Array.pop() removes the last and returns it | |
Array.shift() removes the first and returns it | |
Array.unshift() appends to the front and returns the new length | |
*/ | |
$(function() { | |
function json_ws(path, on_msg) { | |
if (!("WebSocket" in window)) { | |
alert("Use a browser supporting websockets"); | |
} | |
var sock = new WebSocket("ws://" + location.host + path); | |
sock.onmessage = function(msg) { | |
var data; | |
try { | |
data = JSON.parse(msg.data); | |
} | |
catch (SyntaxError) { | |
console.log("Invalid data: " + msg.data); | |
return; | |
} | |
if (data) | |
on_msg(data); | |
} | |
window.onbeforeunload = function() { | |
sock.onclose = function() {}; | |
sock.close(); | |
} | |
} | |
var analog_keys = ['vcell', 'current', 'temp']; | |
(function() { | |
function make_realtime(key) { | |
var buf = [], callbacks = []; | |
return { | |
data: function(ts, val) { | |
buf.push({ts: ts, val: val}); | |
callbacks = callbacks.reduce(function(result, cb) { | |
if (!cb(buf)) | |
result.push(cb); | |
return result | |
}, []); | |
}, | |
add_callback: function(cb) { | |
callbacks.push(cb); | |
} | |
} | |
}; | |
var realtime = { | |
vcell: make_realtime('vcell'), | |
current: make_realtime('current'), | |
temp: make_realtime('temp'), | |
rawcap: make_realtime('rawcap'), | |
}; | |
/* This websocket sends homogenous messages in the form | |
* {timestamp: 1234567, analog: {vcell: 3.3, current: 2.3, temp: 20}} | |
* where timestamp is a Unix timestamp | |
*/ | |
json_ws("/realtime.ws", function(data) { | |
analog_keys.map(function (key) { | |
realtime[key].data(data.timestamp, data.analog[key]); | |
}); | |
}); | |
var context = cubism.context().step(1000).size(960); | |
var metric = function (key, title) { | |
var rt = realtime[key]; | |
return context.metric(function (start, stop, step, callback) { | |
start = start.getTime(); | |
stop = stop.getTime(); | |
rt.add_callback(function(buf) { | |
if (!(buf.length > 1 && | |
buf[buf.length - 1].ts > stop + step)) { | |
// Not ready, wait for more data | |
return false; | |
} | |
var r = d3.range(start, stop, step); | |
/* Don't like using a linear search here, but I don't | |
* know enough about cubism to really optimize. I had | |
* assumed that once a timestamp was requested, it would | |
* never be needed again so I could drop it. That doesn't | |
* seem to be true! | |
*/ | |
var i = 0; | |
var point = buf[i]; | |
callback(null, r.map(function (ts) { | |
if (ts < point.ts) { | |
// We have to drop points if no data is available | |
return null; | |
} | |
for (; buf[i].ts < ts; i++); | |
return buf[i].val; | |
})); | |
// opaque, but this tells the callback handler to | |
// remove this function from its queue | |
return true; | |
}); | |
}, title); | |
}; | |
['top', 'bottom'].map(function (d) { | |
d3.select('#charts').append('div') | |
.attr('class', d + ' axis') | |
.call(context.axis().ticks(12).orient(d)); | |
}); | |
d3.select('#charts').append('div').attr('class', 'rule') | |
.call(context.rule()); | |
charts = { | |
vcell: { | |
title: 'Voltage', | |
unit: 'V', | |
extent: [2.4, 4.4] | |
}, | |
current: { | |
title: 'Current', | |
unit: 'mA', | |
extent: [-5000, 2000] | |
}, | |
temp: { | |
title: 'Temperature', | |
unit: '\u00b0C', | |
extent: [-20, 60] | |
} | |
}; | |
Object.keys(charts).map(function (key) { | |
var cht = charts[key]; | |
var num_fmt = d3.format('.3r'); | |
d3.select('#charts') | |
.insert('div', '.bottom') | |
.datum(metric(key, cht.title)) | |
.attr('class', 'horizon') | |
.call(context.horizon() | |
.extent(cht.extent) | |
.title(cht.title) | |
.format(function (n) { | |
return num_fmt(n) + ' ' + cht.unit; | |
}) | |
); | |
}); | |
context.on('focus', function (i) { | |
if (i !== null) { | |
d3.selectAll('.value').style('right', | |
context.size() - i + 'px'); | |
} | |
else { | |
d3.selectAll('.value').style('right', null) | |
} | |
}); | |
})(); | |
}); | |
</script> | |
<style> | |
html { | |
min-width: 1040px; | |
} | |
body { | |
font-family: 'Helvetica Neue', Helvetica, sans-serif; | |
width: 960px; | |
} | |
.horizon { | |
border-bottom: solid 1px black; | |
overflow: hidden; | |
border-top: solid 1px black; | |
position: relative; | |
} | |
.horizon + .horizon { | |
border-top: none; | |
} | |
.horizon .title, | |
.horizon .value { | |
bottom: 0; | |
line-height: 30px; | |
margin: 0 6px; | |
position: absolute; | |
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); | |
white-space: nowrap; | |
} | |
.horizon .title { | |
left: 0; | |
} | |
.horizon .value { | |
right: 0; | |
} | |
.horizon canvas { | |
display: block; | |
} | |
.axis { | |
font: 10px sans-serif; | |
} | |
.axis text { | |
transition: fill-opacity 250ms linear; | |
} | |
.axis path { | |
display: none; | |
} | |
.line { | |
/* No idea why 8px margin left is needed here :( */ | |
margin-left: 8px; | |
background: black; | |
opacity: 0.2; | |
z-index: 2; | |
} | |
.axis .line { | |
stroke: black; | |
shape-rendering: crispEdges; | |
} | |
</style> | |
</head> | |
<body> | |
<div id='charts'></div> | |
</body> | |
</html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment