function dataSrc(url, config) { config = config || {}; url = url || [ location.protocol == 'https:' ? 'wss://' : 'ws://', location.hostname, location.protocol == 'https:' ? ':' + (location.port || 443) : ':' + (location.port || 80), '/data/stream.ws' ].join(''); var ctx = { 'url': url, 'ws': {}, 'proto': 'binary', 'buffer': '', 'headers': [], 'data': [], 'separatorRows': config.separatorRows || "\n", 'separatorCols': config.separatorCols || "\t", 'ping': (function() { var ac = new(window.AudioContext || window.webkitAudioContext)(); var osc = ac.createOscillator(); var gn = ac.createGain(); osc.connect(gn); gn.connect(ac.destination); gn.gain.setTargetAtTime(0.0, ac.currentTime, 0.0); osc.start(ac.currentTime); return function(freq) { var d = [0.001, 0.01, 0.1]; // relative decay constants: freq < rise < fall e.g. [0.001, 0.1, 0.2] var t = 0.100 // 100 ms target time var v = 0.25 // volume function energy(channel) { var c = [405, 4095]; var e = [29.5503, 3628.78]; return e[0] + (e[1] - e[0]) * (channel - c[0]) / (c[1] - c[0]); }; var f = energy(freq); osc.frequency.setTargetAtTime(f, ac.currentTime + d[0] * t, d[0] * t); gn.gain.setTargetAtTime(v, ac.currentTime + d[1] * t, d[1] * t); gn.gain.setTargetAtTime(0.0, ac.currentTime + d[2] * t, d[2] * t); } })() }; function connect() { if (ctx.proto == 'binary') { ctx.ws = new WebSocket(ctx.url, 'binary'); ctx.ws.binaryType = 'arraybuffer'; } else { ctx.ws = new WebSocket(ctx.url); } ctx.ws.onerror = function(evt) { console.log('WS ERROR: ' + evt.data); }; ctx.ws.onopen = function(evt) { console.log('WS OPENED: ' + ctx.url) }; ctx.ws.onmessage = function(evt) { //console.log('WS DATA:'); ctx.buffer += ctx.proto == 'binary' ? String.fromCharCode.apply(null, new Uint8Array(evt.data)) : evt.data; var rows = ctx.buffer.split(ctx.separatorRows); ctx.buffer = rows.pop(); // buffer incomplete rows for (var r = 0; r < rows.length; r++) { var cols = rows[r].split(ctx.separatorCols); var row = []; for (var c = 0; c < cols.length; c++) { row.push((function(str) { var num = parseFloat(str); c > 0 ? ctx.ping(num) : true; return isNaN(num) ? str : num; })(cols[c])); } if (ctx.headers.length == 0) { ctx.headers = row; } else { ctx.data.push(row); } } }; ctx.ws.onclose = function(evt) { console.log('WS CLOSED:' + evt.code); window.setTimeout(connect, 1000); // retry every 1000 ms }; } connect(); this.getHeaders = function() { // get headers strings array return ctx.headers; }; this.getData = function(len) { // get len datasets and delete old data return ctx.data.splice(0).slice(-1 * (len || 1)); }; return this; } function makeData(src, len) { //console.log('makeData(src,' + len + ')'); len = len || 100; var rows = src.getData(len); var cols = src.getHeaders(); var data = []; var vals = []; for (var c = 0; c < cols.length; c++) { vals = []; for (var r = 0; r < rows.length; r++) { var val = typeof(rows[r][c]) != 'undefined' ? rows[r][c] : null //console.log( cols[c] + '[' + r + '] = ' + val); vals.push(val); } data.push(vals); } var time = data && data.length > 0 ? data.shift() : null; return { 'x': (function(n) { for (x = []; n--; x.push(time)); return x })(cols.length - 1), 'y': data, 'i': (function(n) { for (a = []; n--; a[n] = n); return a; })(cols.length - 1) }; } function formatNames(str, find, replace) { str = str.toString(); find = find || '_'; replace = replace || ' '; return str.replace( new RegExp( find.replace( new RegExp( '([.*+?^=!:${}()|\[\]\/\\])', 'g' ), '\\$1' ), 'g' ), replace ); } function makePlot(ctx, hdr) { //console.log(JSON.stringify(hdr, null, "\t")); var data = []; var cmap = chroma.scale(chroma.bezier(ctx.data.line.color)).mode('lab').correctLightness(false).colors(hdr.length - 1, null); for (var i = 1; i < hdr.length; i++) { var trace = JSON.parse(JSON.stringify(ctx.data)) trace['type'] = 'scatter'; trace['mode'] = 'lines'; trace['fill'] = 'tonexty'; // 'tozeroy'; trace['connectgaps'] = true; trace['name'] = formatNames(hdr[i]); trace['x'] = new Array(ctx.maxPoints); trace['y'] = new Array(ctx.maxPoints); trace['line']['width'] = 1; trace['line']['color'] = cmap[i - 1].css(); trace['fillcolor'] = cmap[i - 1].alpha(0.2).css() data.push(trace); } document.title = ctx.layout.title; return Plotly.plot(ctx.id, data, ctx.layout, ctx.config); // https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_api.js#L53 } function makeConfig(id, ctx) { var cmap = chroma.bezier(ctx.colors.graph).scale().colors(5); var data = { 'name': 'trace', // replaced by tsv column headers 'type': 'scatter', 'mode': 'lines', // 'lines+markers', 'line': { 'shape': ctx.shape, 'smoothing': ctx.smoothing, 'color': ctx.colors.lines, // interpolated by colormap function 'width': ctx.size }, 'marker': { 'color': cmap[2], 'size': 1.5 * ctx.size }, 'y': [0] // replaced by tsv column data }; var layout = { 'title': ctx.title.plot, 'titlefont': { 'family': 'Courier New, monospace', 'size': 24, 'color': cmap[0] }, 'autosize': true, // 'width': window.innerWidth, // 'height': window.innerHeight, 'plot_bgcolor': cmap[4], 'paper_bgcolor': cmap[4], 'xaxis': { 'title': ctx.title.xaxis, 'range': [0, 3650], 'color': cmap[1], 'titlefont': { 'family': 'Courier New, monospace', 'size': 18, 'color': cmap[0] }, 'tickwidth': ctx.size, 'tickcolor': cmap[1], 'linecolor': cmap[1], 'gridcolor': cmap[3], 'zerolinecolor': cmap[1], 'mirror': true }, 'yaxis': { 'title': ctx.title.yaxis, 'type': 'log', 'autorange': true, 'color': cmap[1], 'titlefont': { 'family': 'Courier New, monospace', 'size': 18, 'color': cmap[0] }, 'tickwidth': ctx.size, 'tickcolor': cmap[1], 'linecolor': cmap[1], 'gridcolor': cmap[3], 'zerolinecolor': cmap[1], 'mirror': true }, 'margin': { 'l': 64, 'r': 32, 'b': 64, 't': 80, 'pad': 0 } }; var config = { 'scrollZoom': true, 'showLink': false, 'displaylogo': false, 'modeBarButtonsToAdd': [ { 'name': 'Toggle Lin/Log', 'click': function(gd) { Plotly.relayout(gd, 'yaxis.type', gd.layout.yaxis.type == 'log' ? 'linear' : 'log'); }, 'icon': { 'width': 1000, 'ascent': 850, 'descent': 0, 'path': 'm 598.75596,109.5 c -8.63281,0 -15.62503,-6.99222 -15.62503,-15.624987 V 62.624975 c 0,-8.632812 6.99222,-15.624987 15.62503,-15.624987 H 770.63093 V -77.999996 H 598.75596 c -8.63281,0 -15.62503,-6.99221 -15.62503,-15.62499 v -31.250024 c 0,-8.63283 6.99222,-15.625 15.62503,-15.625 H 770.63093 V -265.49999 H 598.75596 c -8.63281,0 -15.62503,-6.99222 -15.62503,-15.625 v -31.25002 c 0,-8.63283 6.99222,-15.625 15.62503,-15.625 H 770.63093 V -452.99999 H 598.75596 c -8.63281,0 -15.62503,-6.99222 -15.62503,-15.625 v -31.25002 c 0,-8.63283 6.99222,-15.62499 15.62503,-15.62499 H 770.63093 V -640.49999 C 770.63093,-675.01175 742.64268,-703 708.13096,-703 h -375 c -34.51171,0 -62.50001,27.98825 -62.50001,62.50001 v 874.99998 c 0,34.51171 27.9883,62.50001 62.50001,62.50001 h 375 c 34.51172,0 62.49997,-27.9883 62.49997,-62.50001 V 109.5 Z' } } ], 'mmodeBarButtonsToRemove': ['sendDataToCloud'] }; return { 'id': id, 'data': data, 'layout': layout, 'config': config, 'maxPoints': ctx.maxPoints }; } function createGraph(id, ctx) { ctx = { // sane defaults 'src': { 'stream': ctx.src.stream || '/data/stream.ws', 'init': ctx.src.init || '/data/hist.tsv' }, 'title': { 'plot': ctx.title.plot || 'Plot Title', 'xaxis': ctx.title.xaxis || 'Time / s', 'yaxis': ctx.title.yaxis || 'Measured Entity / a.u.' }, 'colors': { 'graph': ctx.colors.graph || ['black', 'white'], 'lines': ctx.colors.lines || ['blue', 'cyan', 'lightgreen', 'yellow', 'red'] }, 'maxPoints': ctx.maxPoints || 100, 'size': ctx.size || 2, 'scale': ctx.scale || 1.5, 'shape': ctx.shape || 'linear', 'smoothing': ctx.smoothing || 1 }; var src = dataSrc([ location.protocol == 'https:' ? 'wss://' : 'ws://', location.hostname, location.protocol == 'https:' ? ':' + (localtion.port || 443) : ':' + (location.port || 80), ctx.src.stream ].join('')); var setup = window.setInterval(function() { // wait for headers to arrive var headers = src.getHeaders(); if (headers.length > 0) { clearInterval(setup); makePlot(makeConfig(id, ctx), headers); function loadInitData(dataUrl) { var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", dataUrl); // xmlhttp.open("GET", url, false); xmlhttp.onreadystatechange = function() { if ((xmlhttp.status == 200) && (xmlhttp.readyState == 4)) { var maxPoints = ctx.maxPoints || 4096; function energy(channel) { var c = [405, 4095]; var e = [29.5503, 3628.78]; return e[0] + (e[1] - e[0]) * (channel - c[0]) / (c[1] - c[0]); }; var data = { 'x': [(function(n) { for (a = []; n--; a[n] = energy(n)); return a; })(maxPoints)], 'y': [ [] ], 'i': [], 't': 0, 'e': 0 }; var initData = xmlhttp.responseText.split("\n").slice(0, maxPoints).map(parseFloat); for (i = 0; i < maxPoints; i++) { // prefill initial data var val = initData[i]; if (isNaN(val)) { data.y[0][i] = 0; } else { data.y[0][i] = val; data.e += val; } } //console.log(data); var res = true; res &= Plotly.relayout(id, { 'title': ctx.title.plot + ' ' + data.t + 's', 'yaxis.title': ctx.title.yaxis + ' / ' + data.e }).catch(function(e) { //console.log('ERROR[Plotly.relayout()]: ' + e); }); res &= Plotly.extendTraces(id, { 'x': data.x, 'y': data.y }, [0], maxPoints).catch(function(e) { //console.log('ERROR[Plotly.extendTraces()]: ' + e); }); var update = window.setInterval(function() { // update plot in realtime var rawdata = makeData(src, 1024 * 1024 * 1024); // get at most 1 GB of events data.i = (function(n) { for (a = []; n--; a[n] = n); return a; })(rawdata.y.length); for (i = 0; i < rawdata.y.length; i++) { for (j = 0; j < rawdata.y[i].length; j++) { data.y[i][rawdata.y[i][j]] += 1; data.e += 1 } } var evtTime = rawdata.x[0][rawdata.x[0].length - 1]; data.t = evtTime ? evtTime : data.t; return data.i.length > 0 ? (function() { var res = true; res &= Plotly.relayout(id, { 'title': ctx.title.plot + ' ' + data.t + 's', 'yaxis.title': ctx.title.yaxis + ' / ' + data.e }).catch(function(e) { //console.log('ERROR[Plotly.relayout()]: ' + e); }); res &= Plotly.extendTraces(id, { 'x': data.x, 'y': data.y }, data.i, maxPoints).catch(function(e) { //console.log('ERROR[Plotly.extendTraces()]: ' + e); }); return res; })() : true; }, 500); // time resolution 500ms } }; xmlhttp.send(); } loadInitData(ctx.src.init); } }, 500); }