Skip to content

Instantly share code, notes, and snippets.

@NormanEdance
Forked from cuadue/cubism-websockets.html
Created February 20, 2018 04:45
Show Gist options
  • Save NormanEdance/4013b8a72fe97ea6f3522114158a8736 to your computer and use it in GitHub Desktop.
Save NormanEdance/4013b8a72fe97ea6f3522114158a8736 to your computer and use it in GitHub Desktop.
Streaming data to cubism.js with websockets
<!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