1913 lines
67 KiB
JavaScript
1913 lines
67 KiB
JavaScript
// Graph
|
|
|
|
/*
|
|
interface:
|
|
|
|
updateCharts2(graph)
|
|
graph is a dict <key> iof array of (t,y)
|
|
|
|
called when data is updated
|
|
|
|
graphs.receivedVars(blocks)
|
|
|
|
block is a list of dict(tag, unit, curves)
|
|
curves is a dict(name, label, color, period)
|
|
|
|
|
|
internals:
|
|
|
|
graphs: containing main objects and methods
|
|
with the arrays:
|
|
graph_array: array fo graphs
|
|
graph_elm_array: the array of elements containing the graphs
|
|
vars_array: the array containing the variables meta data
|
|
|
|
graph = Graph(..): one graph with meta data and methods for one chart (short: ch)
|
|
in loops called gr
|
|
|
|
chart = Chart(...): the chartjs chart
|
|
*/
|
|
|
|
// Unused ?
|
|
function Timer(){
|
|
let start = window.performance.now();
|
|
return function(x = "timer"){
|
|
console.log(x, window.performance.now()-start);
|
|
}
|
|
}
|
|
|
|
// Sets the graph background at loading
|
|
window.addEventListener('load', function(){
|
|
var urlParams = new URLSearchParams(window.location.search);
|
|
if(urlParams.has('white')){
|
|
document.body.classList.add('white');
|
|
}
|
|
else if(urlParams.has('black')){
|
|
document.body.classList.add('black');
|
|
}
|
|
})
|
|
|
|
/*
|
|
function addClass(obj, cls){
|
|
if(!obj.classList.contains(cls))
|
|
obj.classList.add(cls)
|
|
}
|
|
|
|
function delClass(obj, cls){
|
|
if(obj.classList.contains(cls))
|
|
obj.classList.remove(cls)
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Function used to make AJAX request, with three nested functions (POST, GET with returned JSON data, GET)
|
|
* @param {string} addr - The endpoint
|
|
* @returns This
|
|
*/
|
|
function AJAX(addr){
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
if (debugCommunication) console.log('AJAX', addr);
|
|
this.sendJSON = function(array, returnIsJSON = true){
|
|
xhr.open("POST", addr, true);
|
|
xhr.send(JSON.stringify(array));
|
|
return new Promise(function(resolve, reject){
|
|
xhr.addEventListener('load', function(){
|
|
if(this.status == 200){
|
|
if(returnIsJSON){
|
|
this.responseJSON = JSON.parse(this.responseText);
|
|
}
|
|
resolve(this);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
this.getJSON = function(){
|
|
xhr.open("GET", addr, true);
|
|
xhr.send();
|
|
return new Promise(function(resolve, reject){
|
|
xhr.addEventListener('load', function(){
|
|
if(this.status == 200){
|
|
if (debugCommunication) console.log('A RES', JSON.parse(this.responseText));
|
|
resolve(JSON.parse(this.responseText));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
this.get = function(responseType = "text"){
|
|
xhr.open("GET", addr, true);
|
|
xhr.responseType = responseType;
|
|
xhr.send();
|
|
return new Promise(function(resolve, reject){
|
|
xhr.addEventListener('load', function(){
|
|
if(this.status == 200){
|
|
resolve(this);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Performs a callback on double tap (defined as two taps less than 500ms and in a window of +-40px in x and y directions)
|
|
*
|
|
* Timeout things maight be useless...
|
|
*
|
|
* @param {*} callback - The callback to execute
|
|
* @returns - Never used
|
|
*/
|
|
function doubleTap(callback){
|
|
var timeout;
|
|
var lastTap = 0, lastX=NaN, lastY=NaN;
|
|
function handler(event) {
|
|
console.log(event);
|
|
var currentTime = new Date().getTime();
|
|
var tapLength = currentTime - lastTap;
|
|
let touch = event.changedTouches ? event.changedTouches[0] : event,
|
|
x = touch.clientX,
|
|
y = touch.clientY;
|
|
clearTimeout(timeout);
|
|
if (tapLength < 500 && tapLength > 0 && Math.abs(lastX-x) < 40 && Math.abs(lastY-y) < 40) {
|
|
event.preventDefault();
|
|
callback()
|
|
} else {
|
|
timeout = setTimeout(function() {
|
|
clearTimeout(timeout);
|
|
}, 500);
|
|
}
|
|
lastTap = currentTime;
|
|
|
|
lastX = x;
|
|
lastY = y;
|
|
}
|
|
window.addEventListener('touchend', handler);
|
|
return {stop: function(){ window.removeEventListener('touchend', handler) }}
|
|
}
|
|
/**
|
|
* Return the highest value out of a time interval in a set of points
|
|
* @param {*} array - The array we want to look the highest value for
|
|
* @param {*} tmin - The lower bound of the time interval
|
|
* @param {*} tmax - The upper boud of the time interval
|
|
* @returns The highest found value (y value)
|
|
*/
|
|
function maxAr(array, tmin, tmax){
|
|
return Math.max.apply(Math, array.map(function(o) {
|
|
if (o.y == null || o.x < tmin || o.x > tmax) return -1e99;
|
|
return o.y;
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Return the lowest value out of a time interval in a set of points
|
|
* @param {*} array - The array we want to look the lowest value for
|
|
* @param {*} tmin - The lower bound of the time interval
|
|
* @param {*} tmax - The upper boud of the time interval
|
|
* @returns The lowest found value (y value)
|
|
*/
|
|
function minAr(array, tmin, tmax){
|
|
return Math.min.apply(Math, array.map(function(o) {
|
|
if (o.y == null || o.x < tmin || o.x > tmax) return 1e99;
|
|
return o.y;
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Formats the values to display (especially the exponentials)
|
|
* @param {*} val - The value to format
|
|
* @param {number} significant_digits - The max number of digits to display
|
|
* @returns The formatted value as a string
|
|
*/
|
|
function strFormat(val, significant_digits=13) {
|
|
if (val == null) return '';
|
|
evalue = val.toExponential(significant_digits-1).replace(/0*e/, 'e').replace(/\.e/, 'e').replace("e+", "e");
|
|
fvalue = Number.parseFloat(evalue).toString();
|
|
if (fvalue.length <= evalue.length)
|
|
return fvalue;
|
|
else
|
|
return evalue;
|
|
}
|
|
|
|
// Defining keys for global controls
|
|
|
|
let xyKey = "xy-control";
|
|
|
|
let globalControls = (function (){
|
|
let controlsMap = {};
|
|
|
|
function loadControls(panel){
|
|
let controlBar = document.createElement("div");
|
|
controlBar.id = "control_bar";
|
|
panel.appendChild(controlBar);
|
|
|
|
let xyControl = new Control("res/x_zoom_white.png", "res/y_zoom_white.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode);
|
|
|
|
controlBar.appendChild(xyControl);
|
|
|
|
controlsMap[xyKey] = xyControl;
|
|
}
|
|
|
|
function getControlsMap(){
|
|
return controlsMap;
|
|
}
|
|
|
|
return {
|
|
loadControls: loadControls,
|
|
getControlsMap: getControlsMap,
|
|
}
|
|
})();
|
|
|
|
let datesKey = "dates-indicator";
|
|
|
|
let globalIndicators = (function (){
|
|
|
|
let indicatorsMap = {}
|
|
|
|
function loadIndicators(panel){
|
|
let leftDate = Date.now() - 30*60*1000;
|
|
let datesIndicator = new DateIndicator(leftDate, graphs.gotoNow, graphs.jumpToDate);
|
|
|
|
panel.appendChild(datesIndicator);
|
|
datesIndicator.style.marginLeft = "auto";
|
|
datesIndicator.style.marginRight = "auto";
|
|
|
|
indicatorsMap[datesKey] = datesIndicator;
|
|
}
|
|
|
|
function getIndicatorsMap(){
|
|
return indicatorsMap;
|
|
}
|
|
|
|
return {
|
|
loadIndicators: loadIndicators,
|
|
getIndicatorsMap: getIndicatorsMap
|
|
}
|
|
|
|
})()
|
|
|
|
function loadGraphicsMenu(panel){
|
|
|
|
let menuGraphicsPopup = new MenuPopup();
|
|
let removeCursorHelpEntry = new HelpEntry("How to remove the cursor", "You can double click/tap on any graph.");
|
|
menuGraphicsPopup.addEntry(removeCursorHelpEntry);
|
|
|
|
let graphicsMenuControl = new Control("res/menu_white.png", "res/menu_white.png", "Menu", () => {menuGraphicsPopup.show()});
|
|
panel.appendChild(menuGraphicsPopup);
|
|
menuGraphicsPopup.getContainer().style.top = "28px";
|
|
menuGraphicsPopup.getContainer().style.right = "20px";
|
|
panel.appendChild(graphicsMenuControl);
|
|
graphicsMenuControl.style.marginLeft="6px";
|
|
graphicsMenuControl.style.marginRight="6px";
|
|
}
|
|
|
|
let graphs = (function (){
|
|
let dataset_to_graph_map = {}; // a dictionnary mapping a variable name to a two values array, containing its graph index and its position inside the graph
|
|
let blocks, liveMode=true, top_vars=[], bottom_vars=[];
|
|
let legendFlag = false, currentZoomMode = isTouchDevice ? 'xy' : 'x';
|
|
let prevTime = null, prevMin = null, prevMax = null, prevGraph = null; // zoom speed limitation
|
|
let cursorLinePos = null; // the position of the cursor line (given by its x value)
|
|
|
|
let type = 'linear'; // type of graphs axis to display
|
|
|
|
let ngraphs = 0; // current number of graphs
|
|
|
|
let graph_array = []; // an array of Graph objects
|
|
let graph_elm_array = []; // an array of HTML divs (with appropriate classes) containing the corresponding Graph objects of graph_array
|
|
let vars_array = []; // an array of arrays of curve names, each curve names array is positionned at its graph id
|
|
let prev_blk = {};
|
|
let tag_dict = {}; // a dictionnary of graph indexes (corresponding to the three indexes of the above arrays), indexed by the tag of the graphs
|
|
|
|
let currentMinTime = 0, currentMaxTime = 0; // the currently displayed time range
|
|
let startTime; // time of query on server
|
|
let recvTime; // local relative time at receive of above
|
|
let minTime, maxTime; // the queried time range
|
|
let lastTime = 0; // time of most recent data point
|
|
|
|
let resolution = undefined;
|
|
|
|
let activateUpdateTimeout = undefined;
|
|
let updateAutoTimeout = undefined;
|
|
|
|
let container = document.createElement('div');
|
|
container.classList.add("graphs-container");
|
|
|
|
/** The current time corrected for server time */
|
|
function now(){
|
|
return startTime + (performance.now()-recvTime);
|
|
}
|
|
|
|
/**
|
|
* Clears a graph (meaning it deletes the datasets for the corresponding Graph object, set the Graph Object to undefined and empties the HTML container) at the given index
|
|
* @param {number} gindex - The graph index to clear
|
|
*/
|
|
function clear(gindex){
|
|
let graph_elm = graph_elm_array[gindex];
|
|
let graph = graph_array[gindex];
|
|
|
|
graph_elm.innerHTML = '';
|
|
graph_array[gindex] = undefined;
|
|
vars_array[gindex] = [];
|
|
if (graph) {
|
|
for (let key in dataset_to_graph_map) {
|
|
if (dataset_to_graph_map[key][0] == gindex) {
|
|
delete dataset_to_graph_map[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates the "unit" selection block in the given gindex div graph container
|
|
* @param {number} gindex - The id of the graph container element where to display the selection block
|
|
*/
|
|
function createSelection(gindex){
|
|
let graph_elm = graph_elm_array[gindex];
|
|
clear(gindex);
|
|
|
|
for(let tag in tag_dict){
|
|
if (tag_dict[tag] === gindex){ // we indicate that the tag is not displayed
|
|
tag_dict[tag] = -1
|
|
break
|
|
}
|
|
}
|
|
let selection = document.createElement('div');
|
|
selection.classList.add('selection');
|
|
|
|
for (let block of blocks) {
|
|
|
|
let bel = document.createElement('div');
|
|
bel.classList.add('select');
|
|
let title = document.createElement('div');
|
|
title.classList.add('title');
|
|
title.innerHTML = block.tag;
|
|
bel.appendChild(title);
|
|
let params = document.createElement('div');
|
|
params.classList.add('params');
|
|
for(let param of block.curves){
|
|
let pel = document.createElement('div');
|
|
pel.classList.add('param');
|
|
pel.innerHTML = param.label;
|
|
params.appendChild(pel);
|
|
}
|
|
bel.appendChild(params);
|
|
bel.addEventListener('click', function(){
|
|
if (block.tag in tag_dict) { //if the block that we might select is in the ones that are displayed
|
|
let idx = tag_dict[block.tag]; // we get the current gindex (idx) of the clicked block
|
|
if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window
|
|
createSelection(idx); // We will create a selection at the gindex
|
|
}
|
|
}
|
|
createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
|
|
})
|
|
selection.appendChild(bel);
|
|
|
|
}
|
|
graph_elm.appendChild(selection);
|
|
}
|
|
|
|
/**
|
|
* Sets live mode and enable/disable 'go to now' button
|
|
* @param {boolean} mode - Tells if we are in live mode or not
|
|
*/
|
|
function setLiveMode(mode=null) {
|
|
if (mode !== null){
|
|
liveMode = mode;
|
|
}
|
|
if (liveMode && cursorLinePos === null)
|
|
// gotoNowElm.innerHTML = '';
|
|
// globalControls.getControlsMap()[goToNowKey].changeToAlt();
|
|
console.log("Need to change to nothing");
|
|
else
|
|
// gotoNowElm.innerHTML = 'go to now';
|
|
// globalControls.getControlsMap()[goToNowKey].changeToMain();
|
|
console.log("Need to change to seen");
|
|
}
|
|
|
|
/**
|
|
* Calls the /graph route with the variables of the block
|
|
* Adds the received dataset in the graph being created
|
|
* Sets the visualization window, conditionnaly autoscale, applies the changes, and show the legend
|
|
* @param {number} gindex - The position of the graph in the container
|
|
* @param {{tag:string, unit:string, curves:[{name:string, label:string, color:string, original_color:string}]}} block - The information of the block to create
|
|
*/
|
|
function createGraph(gindex, block){
|
|
console.log("clear for create graph", gindex)
|
|
clear(gindex);
|
|
tag_dict[block.tag] = gindex;
|
|
let dict = {} // {string: [name:string, label:string, color:string, original_color:string]}
|
|
for (let curve of block.curves) {
|
|
if (curve.show !== false) {
|
|
vars_array[gindex].push(curve.name);
|
|
dict[curve.name] = curve;
|
|
}
|
|
}
|
|
//let varlist = top_vars.concat(bottom_vars);
|
|
varlist = vars_array[gindex];
|
|
let graph_elm = graph_elm_array[gindex];
|
|
|
|
timeDeltaAxis = maxTime - minTime
|
|
setResolution(timeDeltaAxis)
|
|
|
|
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
|
|
|
|
//console.log('Graph', block, data)
|
|
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
|
graph_array[gindex] = graph;
|
|
|
|
for(let key in data.graph){
|
|
if(!vars_array[gindex].includes(key)){
|
|
continue;
|
|
}
|
|
let pdata = [];
|
|
for(let e of data.graph[key]){
|
|
pdata.push({x: e[0]*1000, y: e[1]});
|
|
}
|
|
if(pdata.length > 0){
|
|
addDataset(gindex, key, pdata, dict[key])
|
|
/*console.log(timeRange);
|
|
if(data[data.length-1].x-data[0].x > d && data[data.length-1].x-data[0].x < (30*60+10)*1000){ // Adjust to requested time
|
|
d = data[data.length-1].x-data[0].x
|
|
max = data[data.length-1].x;
|
|
min = data[0].x;
|
|
}*/
|
|
}
|
|
}
|
|
graph.setMinMax(currentMinTime,currentMaxTime);
|
|
graph.autoScaleIf();
|
|
graph.update();
|
|
showLegends(legendFlag, false);
|
|
if (legendFlag) adjustLegends();
|
|
|
|
if (activateUpdateTimeout !== undefined){
|
|
clearTimeout(activateUpdateTimeout)
|
|
}
|
|
|
|
activateUpdateTimeout = setTimeout(function(){
|
|
activateUpdates();
|
|
}, 1000);
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Adds dataset to the Graph object at gindex position
|
|
* @param {number} gindex - The index of the Graph object to add dataset to
|
|
* @param {string} key - The curve name (variable)
|
|
* @param {*} data - The corresponding data points for this curve
|
|
* @param {*} opts - Some options for the curve (color, label)
|
|
*/
|
|
function addDataset(gindex, key, data, data_opts){
|
|
let graph = graph_array[gindex];
|
|
dataset_to_graph_map[key] = [gindex, graph.addDataset(key, data, data_opts)];
|
|
}
|
|
|
|
/**
|
|
* Sets the y-axis of the chart to scale the curves (window matches the maximum amd minimum of all the curves)
|
|
* @param {*} chart - The chart with whihch to scale the y-axis
|
|
* @returns If the minimun y-value of all the curves of the charts is greater than the maximum y-value (same)
|
|
*/
|
|
function autoScale(chart) {
|
|
axis = chart.options.scales.yAxes[0];
|
|
tax = chart.options.scales.xAxes[0].ticks;
|
|
datasets = chart.data.datasets;
|
|
let max = -1e99;
|
|
let min = 1e99;
|
|
// if there are datasets with values and think lines,
|
|
// consider them only. if not, consider all (second pass in the following loop)
|
|
let extraMin = min;
|
|
let extraMax = max;
|
|
for (let i = 0; i < datasets.length; i++){
|
|
ds = datasets[i];
|
|
if (ds.borderWidth == 1) continue;
|
|
let lmax = maxAr(ds.data, tax.min, tax.max);
|
|
let lmin = minAr(ds.data, tax.min, tax.max);
|
|
if(lmax > max)
|
|
max = lmax;
|
|
if(lmin < min)
|
|
min = lmin;
|
|
if (ds.data.length && liveMode) {
|
|
lasty = ds.data.slice(-1)[0].y;
|
|
if (lasty !== null) {
|
|
extraMin = Math.min(extraMin, lasty);
|
|
extraMax = Math.max(extraMax, lasty);
|
|
}
|
|
}
|
|
}
|
|
if (min > max) return;
|
|
if (min == max) {
|
|
if (min == 0) {
|
|
ystep = 1;
|
|
} else {
|
|
ystep = Math.abs(min * 0.01);
|
|
}
|
|
min -= ystep;
|
|
max += ystep;
|
|
// chart.graph.setLabelDigits(min, max);
|
|
} else {
|
|
ystep = (max - min) * 0.1;
|
|
chart.graph.setLabelDigits(min, max);
|
|
min -= ystep * 0.02;
|
|
max += ystep * 0.02;
|
|
if (liveMode) {
|
|
extraMin -= ystep;
|
|
extraMax += ystep;
|
|
}
|
|
extraMin = Math.min(min - ystep * 0.5, extraMin);
|
|
extraMax = Math.max(max + ystep * 0.5, extraMax);
|
|
if (min >= axis.ticks.min && axis.ticks.min >= extraMin &&
|
|
max <= axis.ticks.max && axis.ticks.max <= extraMax) {
|
|
//console.log('NOCHANGE', max, axis.ticks.max, extraMax)
|
|
return; // do not yet change
|
|
}
|
|
//console.log('CHANMIN', min, axis.ticks.min, extraMin)
|
|
//console.log('CHANMAX', max, axis.ticks.max, extraMax)
|
|
min = extraMin;
|
|
max = extraMax;
|
|
}
|
|
axis.min = axis.ticks.min = min;
|
|
axis.max = axis.ticks.max = max;
|
|
}
|
|
|
|
/**
|
|
* Sets the current viewing window to min and max for each graph
|
|
* @param {number} min - The minimum timestamp in milliseconds of the viewing window
|
|
* @param {number} max - The maximum timestamp in milliseconds of the viewing window
|
|
*/
|
|
function setMinMax(min, max){
|
|
currentMaxTime = max;
|
|
currentMinTime = min;
|
|
globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);
|
|
for (let gr of graph_array) {
|
|
if (gr) gr.setMinMax(min, max);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a data point to the variable's curve
|
|
* @param {string} key - The name of the variable the data point is being added to
|
|
* @param {{x: number, y: number}} data - The new data point to append
|
|
* @returns If the variable is not in the displayed graphs
|
|
* */
|
|
function newDataHandler(key, data){
|
|
if(!(key in dataset_to_graph_map))
|
|
return
|
|
lastTime = Math.max(lastTime, data.x);
|
|
let i = dataset_to_graph_map[key];
|
|
graph_array[i[0]].pushData(i[1], data)
|
|
}
|
|
|
|
/**
|
|
* Function called when the graph container is clicked
|
|
* Shows the legend, shows the go to now button, put a cursor then applies the changes
|
|
* If the click is located on the legend, brings it to the front
|
|
* @param {*} evt - The JS event triggered by the click
|
|
*/
|
|
function clickHandler(evt) {
|
|
if(evt.target.tagName == "CANVAS"){
|
|
legendFlag = true;
|
|
let trect = evt.target.getBoundingClientRect();
|
|
let X = evt.clientX - trect.x, Y = evt.clientY - trect.y;
|
|
showLegends(true, false);
|
|
cursorLine(X);
|
|
setLiveMode();
|
|
update();
|
|
for (let gr of graph_array.slice(0, ngraphs)) {
|
|
if (gr && gr.chart.canvas == evt.target) {
|
|
bringToFront(gr.legend);
|
|
}
|
|
/* clickHandler(evt); */
|
|
}
|
|
}
|
|
}
|
|
container.addEventListener('click', clickHandler)
|
|
|
|
/**
|
|
* Sets (overwrite) the data (curve) of the given variable
|
|
* @param {string} key - The name of the variable to set the data for
|
|
* @param {[{x: number y: number}]} data - The data to set
|
|
* @returns If the variable is not in the graphs
|
|
*/
|
|
function setDataFromKey(key, data){
|
|
if(!(key in dataset_to_graph_map))
|
|
return
|
|
let i = dataset_to_graph_map[key];
|
|
graph_array[i[0]].setData(i[1], data);
|
|
}
|
|
|
|
/**
|
|
* Gets the available variables
|
|
* @returns An array of strings containing the name of the available variables
|
|
*/
|
|
function variables() {
|
|
let vardict = {};
|
|
for (let vars of vars_array) {
|
|
for (let v of vars) {
|
|
vardict[v] = 1;
|
|
}
|
|
}
|
|
return Object.keys(vardict);
|
|
}
|
|
|
|
/**
|
|
* Gets the curve for all the variables within a timerange then updates the charts. Called in checkReload
|
|
* This method is almost the same as createGraph. The main difference is that this one does not create new dataset for the variable, but sets its values (overwrite)
|
|
* @param {number} min - The lower bound of the timerange, which is a unix timestamp in milliseconds
|
|
* @param {number} max - The upper bound of previous
|
|
*/
|
|
function reloadData(min, max){
|
|
|
|
min = min/1000;
|
|
if(max > now()){
|
|
max = 0;
|
|
}else{
|
|
max = max/1000;
|
|
}
|
|
|
|
timeDelta = currentMaxTime - currentMinTime
|
|
setResolution(timeDelta)
|
|
|
|
AJAX("http://" + hostPort + "/graph?time=" + min + ","+max+"&variables=" + variables() + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
|
|
for(let key in data.graph){
|
|
let pdata = [];
|
|
for(let e of data.graph[key]){
|
|
//if(e[0] == null || e[1] == null){
|
|
// continue;
|
|
//}
|
|
pdata.push({x: e[0]*1000, y: e[1]});
|
|
}
|
|
if(pdata.length > 0){
|
|
setDataFromKey(key, pdata);
|
|
}
|
|
}
|
|
console.log("RELOAD")
|
|
activateUpdates();
|
|
|
|
updateAuto(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* If the visualisation window is bigger than the time extremas of the curves, ask for the data, then autoscale if needed and apply the changes
|
|
* Called in updateAuto (conditionnal)
|
|
* @param {*} graph - The graph object to check if reload is needed
|
|
* @returns When data is received (no need to autoScale and update as it is done in reloadData)
|
|
*/
|
|
function checkReload(graph){
|
|
let tk = graph.chart.options.scales.xAxes[0].ticks;
|
|
let xmin = tk.min, xmax = tk.max;
|
|
/*
|
|
if (xmax < now()-100000) { // was 100000 = 100sec
|
|
if (liveMode) console.log('UPDATES OFF?')
|
|
//doUpdates = false;
|
|
}else{
|
|
if (!liveMode) console.log('UPDATES ON?')
|
|
//doUpdates = true;
|
|
}*/
|
|
if (xmin < minTime || (!liveMode && xmax > maxTime) || xmax - xmin < 0.5 * (maxTime - minTime)) {
|
|
//TODO: the criterium for getting finer resolution data should depend, if better res. is available
|
|
// this information has to come from the server
|
|
console.log('reloadData (range change)', xmin - minTime, maxTime - xmax, (xmax - xmin) / (maxTime - minTime))
|
|
reloadData(xmin, xmax);
|
|
minTime = xmin;
|
|
maxTime = xmax;
|
|
return; // autoScale amd update are done when receiving data
|
|
}
|
|
graph.autoScaleIf();
|
|
graph.update();
|
|
}
|
|
|
|
/**
|
|
* Triggers the update of each graph
|
|
*/
|
|
function update(){
|
|
for (let gr of graph_array.slice(0, ngraphs)) {
|
|
if (gr) gr.update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function called at each mouse wheel step.
|
|
* If zoom mode in x mode, disables liveMode if the visualisaiton window is older than the last known value and sets axis min and max
|
|
* If in y mode, disable the autoscale
|
|
* then applies the changes
|
|
* @param {*} graph - The graph Object on which the zoom callback has to be called
|
|
*/
|
|
function zoomCallback(graph){
|
|
let tk, min, max;
|
|
if (currentZoomMode == 'y') {
|
|
tk = graph.chart.options.scales.yAxes[0].ticks;
|
|
} else {
|
|
tk = graph.chart.options.scales.xAxes[0].ticks;
|
|
}
|
|
min = tk.min;
|
|
max = tk.max;
|
|
if (!isTouchDevice) {
|
|
/*
|
|
if (prevGraph != graph) {
|
|
prevTime = null;
|
|
}
|
|
if (prevTime !== null) {
|
|
// slow down (relevant when using touch pad)
|
|
fact = (max - min) / (prevMax - prevMin);
|
|
maxFact = 1 + (performance.now() - prevTime) * 0.001;
|
|
let w = 1;
|
|
if (fact > maxFact) {
|
|
w = (maxFact - 1) / (fact - 1);
|
|
} else if (fact < 1/maxFact) {
|
|
w = (maxFact - 1) / (1 / fact - 1);
|
|
}
|
|
min = prevMin + (min - prevMin) * w;
|
|
max = prevMax + (max - prevMax) * w;
|
|
}
|
|
prevMin = min; prevMax = max;
|
|
prevTime = performance.now(); prevGraph = graph;
|
|
*/
|
|
}
|
|
if (currentZoomMode == 'y') {
|
|
tk.min = min;
|
|
tk.max = max;
|
|
graph.setAutoScale(false);
|
|
} else {
|
|
if (liveMode && max < lastTime) setLiveMode(false);
|
|
setMinMax(min, max);
|
|
}
|
|
console.log('zoomed')
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* Sets the resolution of the viewing window in milliseconds
|
|
* @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
|
|
*/
|
|
function setResolution(timeDelta){
|
|
resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width))
|
|
}
|
|
|
|
function jumpToDate(dateTimestampMs, timeValueMs, mode){
|
|
|
|
cursorLine(null);
|
|
|
|
let msLeftTimestampGetVars = 0, msLeftTimestampGetGraph = 0 , msRightTimestampGetVars = 0, msRightTimestampGetGraph = 0;
|
|
|
|
msLeftTimestampGetVars = dateTimestampMs;
|
|
msLeftTimestampGetGraph = dateTimestampMs;
|
|
|
|
if(mode == DatesPopup.TIME){
|
|
msRightTimestampGetVars = dateTimestampMs + timeValueMs;
|
|
}
|
|
else if(mode == DatesPopup.ALL){ // we ignore time
|
|
msRightTimestampGetVars = dateTimestampMs + 24*60*60*1000 - 1000; // we exclude the very beginning of the next day
|
|
}
|
|
|
|
msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000 - 1000;
|
|
|
|
AJAX("http://" + hostPort + "/getvars?time=" + msLeftTimestampGetVars/1000 + "," + msRightTimestampGetVars/1000 + "&all=" +allQueryParameterRepresentation(mode) + "&id="+ clientID).getJSON().then(function(data){
|
|
blocks = data.blocks;
|
|
|
|
maxTime = msRightTimestampGetGraph;
|
|
minTime = msLeftTimestampGetGraph;
|
|
|
|
currentMaxTime = maxTime + 60000;
|
|
currentMinTime = minTime;
|
|
|
|
globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);
|
|
|
|
ngraphs = 0; // forces all the graphs to reset
|
|
createGraphs();
|
|
|
|
if (liveMode && msRightTimestampGetGraph < lastTime) setLiveMode(false);
|
|
});
|
|
|
|
}
|
|
|
|
function allQueryParameterRepresentation(mode){
|
|
if(mode == DatesPopup.ALL){
|
|
return "True";
|
|
}
|
|
else {
|
|
return "False";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The function called when the viewing window is moved by the mouse.
|
|
*
|
|
* Disables live mode if the new max time of the viewing window is lesser than the last known value,
|
|
* then sets the min ans max time axis for all the graphs
|
|
* then applies changes
|
|
* @param {*} graph - The graph for which the function has to be called
|
|
*/
|
|
function panCallback(graph){
|
|
let tk = graph.chart.options.scales.xAxes[0].ticks;
|
|
let xmin = tk.min, xmax = tk.max;
|
|
if (liveMode && xmax < lastTime) setLiveMode(false);
|
|
setMinMax(xmin,xmax);
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* Sets the viewing window for all graphs if upper bound of the displayed time range is greater than now in live mode,
|
|
* then checks if the data has to be reloaded or simply conditionnaly autoscales then applies the changes
|
|
* @param {*} check - Tells if the data has to be potentially reloaded
|
|
*/
|
|
function updateAuto(check=true){
|
|
if (liveMode) {
|
|
max = now();
|
|
if (currentMaxTime && max > currentMaxTime) {
|
|
max = currentMaxTime + Math.min(60000, 0.1 * (currentMaxTime - currentMinTime));
|
|
setMinMax(currentMinTime, max);
|
|
//reloadData(currentMinTime, max);
|
|
//check = false;
|
|
}
|
|
}
|
|
for (let gr of graph_array.slice(0, ngraphs)) {
|
|
if (gr) {
|
|
if (check) { // autoscaleIf and update are done in checkReload
|
|
checkReload(gr);
|
|
} else {
|
|
gr.autoScaleIf();
|
|
gr.update();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the cursor, gets the var + graphs for now (window visualisation to [now-30min, now]) and ask for updates
|
|
*/
|
|
function gotoNow() {
|
|
let msRightTimestamp = graphs.now();
|
|
let msLeftTimestamp = msRightTimestamp - 30*60*1000;
|
|
cursorLine(null);
|
|
|
|
AJAX("http://" + hostPort + "/getvars?time=" + msLeftTimestamp/1000 + "," + msRightTimestamp/1000 + "&all=False&id="+ clientID).getJSON().then(function(data){
|
|
currentMaxTime = msRightTimestamp + 60000;
|
|
currentMinTime = msLeftTimestamp;
|
|
|
|
maxTime = msRightTimestamp;
|
|
minTime = msLeftTimestamp;
|
|
|
|
blocks = data.blocks;
|
|
|
|
ngraphs = 0;
|
|
createGraphs();
|
|
globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime);
|
|
|
|
});
|
|
}
|
|
|
|
/*
|
|
let g_varlist = [];
|
|
|
|
function getVarlist(blocks){
|
|
var varlist = [];
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
for (var j = 0; j < blocks[i].curves.length; j++) {
|
|
varlist.push(blocks[i].curves[j].name);
|
|
}
|
|
}
|
|
return varlist;
|
|
}
|
|
*/
|
|
|
|
function buildGraphicsUI(){
|
|
|
|
let f = 0;
|
|
insertSlide(f, " ", "graphics", container);
|
|
|
|
let currentSwiper = swiper[f];
|
|
|
|
function setSlidingMode(mode) {
|
|
currentSwiper.params.noSwipingClass = mode ? "allow-swipe" : "swiper-slide-main";
|
|
}
|
|
|
|
currentSwiper.enableSwiping(false);
|
|
currentSwiper.on('reachBeginning', function () {
|
|
currentSwiper.enableSwiping(false);
|
|
})
|
|
|
|
let graphicsPanel = container.parentNode.querySelector('.panel')
|
|
graphicsPanel.classList.add('graphics');
|
|
graphicsPanel.childNodes[0].style.visibility = "hidden"; // hides the span added by the swippers
|
|
// The cross to display "main" panel at the location of the graphs
|
|
let gotoMainElm = document.createElement('div');
|
|
gotoMainElm.innerHTML = "×";
|
|
gotoMainElm.addEventListener('click', function () {
|
|
currentSwiper.enableSwiping(true);
|
|
console.log("MAIN")
|
|
currentSwiper.slideNext();
|
|
});
|
|
|
|
globalIndicators.loadIndicators(graphicsPanel);
|
|
globalControls.loadControls(graphicsPanel);
|
|
loadGraphicsMenu(graphicsPanel);
|
|
graphicsPanel.appendChild(gotoMainElm);
|
|
gotoMainElm.style.marginTop = "auto";
|
|
gotoMainElm.style.marginBottom = "auto";
|
|
gotoMainElm.style.marginRight = "6px";
|
|
gotoMainElm.style.color = "white";
|
|
gotoMainElm.style.cursor = "pointer";
|
|
|
|
if (isTouchDevice) {
|
|
doubleTap(removeCursor);
|
|
} else {
|
|
window.addEventListener('dblclick', removeCursor);
|
|
showLegends(true, false);
|
|
adjustLegends();
|
|
}
|
|
}
|
|
|
|
function initGraphs(blocks){
|
|
buildGraphicsUI();
|
|
receivedVars(blocks);
|
|
}
|
|
|
|
/**
|
|
* Holds the received variables from the /getvars call, gets the server time, insert slide (?),
|
|
* create the graphs, activate SSE graph-update messages, sets the different event listeners
|
|
* @param {[{tag:string, unit:string, curves:[{name:string, label:string, color:string, original_color:string}]}]} blocks_arg -
|
|
*/
|
|
function receivedVars(blocks_arg){
|
|
maxTime = timeRange[1]*1000;
|
|
minTime = timeRange[0]*1000;
|
|
if (currentMaxTime == 0) {
|
|
currentMaxTime = maxTime;
|
|
currentMinTime = minTime;
|
|
}
|
|
AJAX("http://" + hostPort + "/gettime?time=-1800,0&id="+ clientID).getJSON().then(function(data){
|
|
startTime = data.time[1]*1000;
|
|
maxTime = startTime;
|
|
currentMaxTime = maxTime + 60000;
|
|
// console.log('MAXTIME', currentMaxTime - Date.now());
|
|
minTime = data.time[0]*1000;
|
|
recvTime = performance.now();
|
|
|
|
blocks = blocks_arg;
|
|
|
|
createGraphs();
|
|
|
|
});
|
|
}
|
|
|
|
|
|
function setInnerZoomMode(){
|
|
if (currentZoomMode == 'y') {
|
|
globalControls.getControlsMap()[xyKey].changeToAlt();
|
|
} else {
|
|
globalControls.getControlsMap()[xyKey].changeToMain();
|
|
}
|
|
prevTime = null; // reset zoom speed time
|
|
}
|
|
|
|
// Callbacks
|
|
function toggleZoomMode(){
|
|
if (currentZoomMode == 'y')
|
|
currentZoomMode = 'x';
|
|
else
|
|
currentZoomMode = 'y';
|
|
setInnerZoomMode();
|
|
for (let gr of graph_array) {
|
|
if (gr) gr.setZoomMode(currentZoomMode);
|
|
}
|
|
}
|
|
|
|
function removeCursor(evt=null) {
|
|
graphs.cursorLine(null);
|
|
graphs.update();
|
|
}
|
|
|
|
/**
|
|
* New function to activate the graph updates (API call to enbale SSE on the server for the curves)
|
|
*/
|
|
function activateUpdates(){
|
|
|
|
result = AJAX( "http://" + hostPort +
|
|
"/updategraph?" +
|
|
"id=" + clientID).getJSON().then(function(data) {
|
|
setLiveMode(data.live);
|
|
console.log('LIVE create', liveMode)
|
|
})
|
|
|
|
}
|
|
|
|
// Obvious
|
|
function getBlocks(){
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* Triggers the showLegend method of each graph and updates them if needed
|
|
* @param {*} flag - Tells if the legends has to be shown
|
|
* @param {*} doUpdate - Tells if the graphs have to update
|
|
*/
|
|
function showLegends(flag, doUpdate) {
|
|
for (let gr of graph_array) {
|
|
if (gr) {
|
|
gr.showLegend(flag);
|
|
if (doUpdate) gr.update();
|
|
}
|
|
}
|
|
legendFlag = flag;
|
|
//bringToFront(null);
|
|
}
|
|
|
|
/**
|
|
* Adjust the legend window depending on the position of the last legend
|
|
*/
|
|
function adjustLegends() {
|
|
let lastx = 0, lasty = 0, lasty0 = 0, lasty1 = 0, miny = 0;
|
|
let ys = 22;
|
|
for (let gr of graph_array.slice(0, ngraphs)) {
|
|
if (gr) {
|
|
prect = gr.legend.parentNode.getBoundingClientRect();
|
|
lrect = gr.legend.getBoundingClientRect();
|
|
if (miny == 0) miny = prect.top;
|
|
let x = prect.left, y = Math.max(miny, Math.min(prect.top, prect.bottom - lrect.height));
|
|
if (y < lasty) {
|
|
x = (lasty0 < lasty1) ? prect.left : lastx;
|
|
}
|
|
gr.positionLegend(x - prect.left, y - prect.top);
|
|
lastx = lrect.right;
|
|
lasty = lrect.bottom;
|
|
if (x < lrect.width / 2) {
|
|
lasty0 = lasty;
|
|
} else {
|
|
lasty1 = lasty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the graphs or selection for each displayed graph
|
|
*/
|
|
function createGraphs() {
|
|
let n = Math.max(2, Math.floor(window.innerHeight / 200));
|
|
|
|
if (n != ngraphs) {
|
|
for (let i = ngraphs; i < n; i++) {
|
|
if (i >= graph_elm_array.length) {
|
|
let graph_elm = document.createElement('div');
|
|
graph_elm.classList.add('graph');
|
|
graph_elm.classList.add('graph-' + i);
|
|
container.appendChild(graph_elm);
|
|
graph_elm_array[i] = graph_elm;
|
|
} else {
|
|
graph_elm_array[i].style.display = 'flex';
|
|
}
|
|
let shown = false;
|
|
for (block of blocks) {
|
|
if (block.tag in tag_dict) {
|
|
idx = tag_dict[block.tag];
|
|
if (idx < ngraphs) continue;
|
|
}
|
|
// this block is either not shown or only in a hidden graph
|
|
for (let curve of block.curves) {
|
|
if (curve.show !== false) shown = true;
|
|
}
|
|
if (shown) break;
|
|
}
|
|
if (shown) {
|
|
createGraph(i, blocks[i]);
|
|
} else {
|
|
createSelection(i);
|
|
}
|
|
}
|
|
for (let i = n; i < graph_elm_array.length; i++) {
|
|
clear(i);
|
|
graph_elm_array[i].style.display = 'none';
|
|
}
|
|
let hs = Math.floor(100 / (n + 1) + 1);
|
|
let h = (100 - (n-1) * hs); // first height
|
|
ngraphs = n;
|
|
for (el of graph_elm_array) {
|
|
el.style.height = h + '%';
|
|
h = hs;
|
|
}
|
|
}
|
|
}
|
|
|
|
let cursorElement = document.createElement('div')
|
|
cursorElement.classList.add('cursorline');
|
|
container.appendChild(cursorElement);
|
|
|
|
/**
|
|
* Displays the cursor line at the given x position
|
|
* @param {number} setpos - The x position in pixels of the cursor to display
|
|
* @param {boolean} query - (Never set to true in any of the calls)
|
|
* @returns The x position in pixels of the cursor
|
|
*/
|
|
function cursorLine(setpos, query=false) {
|
|
if (query) {
|
|
if (!legendFlag) return null;
|
|
if (cursorLinePos === null) return setpos;
|
|
return cursorLinePos;
|
|
}
|
|
cursorLinePos = setpos;
|
|
if (setpos === null) {
|
|
cursorElement.style.display = 'none';
|
|
} else {
|
|
cursorElement.style.display = 'block';
|
|
cursorElement.style.left = (cursorLinePos - 1) + 'px';
|
|
cursorElement.style.height = window.innerHeight + 'px';
|
|
}
|
|
return cursorLinePos;
|
|
}
|
|
|
|
/**
|
|
* On resize, recreate the graphs and their legend, amd removes the cursor if displayed
|
|
*/
|
|
function resizeHandler() {
|
|
if (blocks) { // prevent error when graphics are not used
|
|
createGraphs();
|
|
adjustLegends();
|
|
if (cursorLinePos) {
|
|
// we do not want to update values -> remove cursor line
|
|
cursorLine(null);
|
|
update();
|
|
}
|
|
}
|
|
}
|
|
window.addEventListener('resize', resizeHandler);
|
|
|
|
let frontLegend = null;
|
|
|
|
/**
|
|
* Brings the given legend to the front
|
|
* @param {*} legend - The legend to put at the front
|
|
*/
|
|
function bringToFront(legend) {
|
|
if (legend != frontLegend) {
|
|
if (frontLegend) {
|
|
frontLegend.style.zIndex = 0;
|
|
//frontLegend.style.opacity = "0.7";
|
|
}
|
|
if (legend) {
|
|
legend.style.zIndex = 1;
|
|
//legend.style.opacity = "1";
|
|
}
|
|
frontLegend = legend;
|
|
}
|
|
}
|
|
|
|
function onZoomCompleteCallback(){
|
|
if (updateAutoTimeout === undefined){
|
|
updateAutoTimeout = setTimeout(function() {
|
|
updateAuto();
|
|
updateAutoTimeout = undefined;
|
|
}, 100);
|
|
}
|
|
else{
|
|
clearTimeout(updateAutoTimeout);
|
|
updateAutoTimeout = undefined;
|
|
}
|
|
}
|
|
|
|
return {
|
|
addDataset: addDataset,
|
|
newDataHandler: newDataHandler,
|
|
zoomCallback: zoomCallback,
|
|
panCallback: panCallback,
|
|
receivedVars: receivedVars,
|
|
createSelection: createSelection,
|
|
createGraphs: createGraphs,
|
|
doUpdates: function(){return liveMode},
|
|
update: update,
|
|
updateAuto: updateAuto,
|
|
now: now,
|
|
checkReload: checkReload,
|
|
getBlocks: getBlocks,
|
|
createGraph: createGraph,
|
|
autoScale: autoScale,
|
|
showLegends: showLegends,
|
|
setLiveMode: setLiveMode,
|
|
cursorLine: cursorLine,
|
|
bringToFront: bringToFront,
|
|
|
|
gotoNow: gotoNow,
|
|
toggleZoomMode: toggleZoomMode,
|
|
jumpToDate: jumpToDate,
|
|
initGraphs: initGraphs,
|
|
onZoomCompleteCallback:onZoomCompleteCallback,
|
|
|
|
prev_blk:prev_blk,
|
|
tag_dict:tag_dict
|
|
}
|
|
})();
|
|
|
|
function Graph(gindex, container, x_label, y_label, tag, scaleType = "linear"){
|
|
let chart;
|
|
|
|
let maxspan = 10 * 864e5, minspan = 120000;
|
|
let autoScaleFlag = true;
|
|
let labelDigits = 6, labelMinWidth = 0, labelLongValue = '';
|
|
|
|
let parent = document.createElement("div");
|
|
parent.classList.add("chart-container");
|
|
|
|
let dselect = document.createElement('div'); //the clickable unit to select the curves we want to display
|
|
dselect.classList.add('dselect');
|
|
dselect.innerHTML = "[" + y_label + "]";
|
|
dselect.addEventListener('click', function(e){
|
|
graphs.createSelection(gindex);
|
|
})
|
|
parent.appendChild(dselect);
|
|
|
|
container.appendChild(parent);
|
|
|
|
let canvas = document.createElement("canvas");
|
|
canvas.setAttribute("width", "500");
|
|
canvas.setAttribute("height", "500");
|
|
canvas.style.width = "500px";
|
|
canvas.style.height = "500px";
|
|
parent.appendChild(canvas);
|
|
let ctx = canvas.getContext("2d");
|
|
let self = this;
|
|
chart = new Chart(ctx, {
|
|
type: 'scatter',
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation:{duration:0},
|
|
scales: {
|
|
yAxes: [{ticks: {
|
|
beginAtZero: false,
|
|
mirror: true,
|
|
padding: -10,
|
|
//workaround for proper number format
|
|
callback: function(label, index, labels) {
|
|
if(index == 0 || index == labels.length-1)
|
|
return "";
|
|
return strFormat(label);
|
|
}
|
|
},
|
|
gridLines:{drawTicks:false},
|
|
scaleLabel: false, // {display: true, labelString: y_label},
|
|
type: scaleType,
|
|
position: 'right',
|
|
afterBuildTicks: function(axis, ticks) {
|
|
if (scaleType == "logarithmic" && ticks.length <= 4) {
|
|
y1 = ticks[0];
|
|
y0 = ticks.slice(-1)[0];
|
|
span = y1 - y0;
|
|
step = Math.abs(span * 0.3).toExponential(0);
|
|
if (step[0] > '5') {
|
|
step = '5' + step.substr(1);
|
|
} else if (step[0] > '2') {
|
|
step = '2' + step.substr(1);
|
|
}
|
|
step = Number.parseFloat(step);
|
|
ticks = [y1];
|
|
for (let yt = Math.ceil(y1 / step) * step; yt > y0; yt -= step) {
|
|
ticks.push(yt);
|
|
}
|
|
ticks.push(y0);
|
|
}
|
|
return ticks
|
|
},
|
|
}],
|
|
xAxes: [{
|
|
scaleLabel: false,//{display: true, labelString: x_label},
|
|
type: 'time',
|
|
time: {
|
|
displayFormats: {'millisecond': 'HH:mm:ss.SSS', 'second': 'HH:mm:ss', 'minute': 'HH:mm','hour': 'dd HH:mm', 'day': 'dd MMM DD', 'week': 'MMM DD', 'month': 'MMM DD'},
|
|
},
|
|
ticks: { padding: -20,
|
|
callback: function(label, index, labels) {
|
|
let l = labels.length - 1;
|
|
if (index == 0 || index == l) return "";
|
|
if (index == 1 || index == l - 1) {
|
|
// skip first and / or last label, if too close to the end
|
|
let minstep = 0.05 * (labels[l].value - labels[0].value);
|
|
if (index == 1) {
|
|
if (labels[1].value - labels[0].value < minstep) return "";
|
|
} else {
|
|
if (labels[l].value - labels[l-1].value < minstep) return "";
|
|
}
|
|
}
|
|
hourofday = /\S+ (\d+:00)/.exec(label);
|
|
if (hourofday && hourofday[1] != '00:00') {
|
|
return hourofday[1];
|
|
}
|
|
return label;
|
|
}
|
|
},
|
|
afterBuildTicks: function(axis, ticks) {
|
|
if (!ticks || ticks.length <= 2) return ticks;
|
|
first = ticks[0].value;
|
|
step = ticks[1].value - first;
|
|
offset = (first - axis._adapter.startOf(first, 'day')) % step;
|
|
let start = 0;
|
|
if (ticks[0].value - offset < axis.min) start = 1;
|
|
let v = axis.min;
|
|
result = [{value: v, major: false}];
|
|
for (tick of ticks.slice(start)) {
|
|
v = tick.value - offset;
|
|
result.push({value: v, major: false});
|
|
}
|
|
v += step;
|
|
if (v < axis.max) result.push({value:v, major: false});
|
|
result.push({value: axis.max, major: false});
|
|
return result;
|
|
},
|
|
gridLines:{drawTicks:false},
|
|
}],
|
|
},
|
|
tooltips: false,
|
|
legend: false,
|
|
pan: {
|
|
enabled: true,
|
|
mode: 'xy',
|
|
speed: 10,
|
|
threshold: 10,
|
|
onPan: function({chart}) { graphs.panCallback(chart.graph);},
|
|
//onPanComplete: function({chart}){graphs.checkReload(chart.graph);redraw()},
|
|
onPanComplete: function({chart}){graphs.updateAuto();},
|
|
},
|
|
zoom: {
|
|
enabled: true,
|
|
drag: false,
|
|
mode: isTouchDevice ? 'xy': 'x',
|
|
speed: 0.1,
|
|
sensitivity: 1,
|
|
onZoom: function({chart}) { graphs.zoomCallback(chart.graph);},
|
|
//onZoomComplete: function({chart}){graphs.checkReload(chart.graph);redraw()},
|
|
onZoomComplete: function({chart}){graphs.onZoomCompleteCallback()},
|
|
}
|
|
}
|
|
});
|
|
|
|
//console.log('create legend')
|
|
let legend = document.createElement('div');
|
|
legend.classList.add('legend');
|
|
parent.appendChild(legend);
|
|
|
|
let legendAnchor = document.createElement('div');
|
|
legendAnchor.classList.add('legendanchor');
|
|
parent.appendChild(legendAnchor);
|
|
let hasLegendAnchor = false;
|
|
//graphSwiper.appendSlide(parent);
|
|
let controls = document.createElement('div');
|
|
controls.classList.add('controls');
|
|
legend.appendChild(controls);
|
|
|
|
/**
|
|
* Adds the HTML content into the contols (legend top bar) of each legend, with a callback when the added control is clicked
|
|
* @param {*} inner - The HTML content to add
|
|
* @param {*} callback - The callback triggered on click
|
|
* @returns The added HTML content with its callback
|
|
*/
|
|
function addControl(inner, callback){
|
|
let c = document.createElement('div');
|
|
c.classList.add('control');
|
|
//c.classList.add('vcontrol');
|
|
c.innerHTML = inner;
|
|
c.addEventListener('click', function(e){
|
|
if(!legendmoving)
|
|
callback(e);
|
|
})
|
|
controls.appendChild(c);
|
|
let sp = document.createElement('div');
|
|
sp.classList.add('spacer');
|
|
controls.appendChild(sp);
|
|
return c;
|
|
}
|
|
|
|
/*changecontrol = addControl("Change Dataset", function(){
|
|
graphs.createSelection(gindex);
|
|
});*/
|
|
|
|
hideBox = document.createElement('div');
|
|
hideBox.innerHTML = '×';
|
|
hideBox.classList.add('control');
|
|
hideBox.classList.add('hidebox');
|
|
hideBox.addEventListener('click', function () {graphs.showLegends(false, true);});
|
|
|
|
controls.appendChild(hideBox);
|
|
|
|
/*
|
|
addControl("Hide legend", function(){
|
|
legend.style.display = 'none';
|
|
redrawX = null;
|
|
});
|
|
*/
|
|
|
|
/*let update_max = null;
|
|
addControl("Reset Zoom/Pan", function(){
|
|
if(update_max !== null){
|
|
chart.options.scales.xAxes[0].ticks.max = update_max;
|
|
update_max = null;
|
|
}
|
|
chart.resetZoom();
|
|
graphs.zoompan(chart.graph);
|
|
});*/
|
|
|
|
let autoScaleRow = addControl("<strong>☒</strong> autoscale", function(){
|
|
setAutoScale(!autoScaleFlag);
|
|
});
|
|
|
|
let linlog = addControl("<strong>☐</strong> log", function(e){
|
|
//graphs.toggleAxesType();
|
|
toggleAxesType();
|
|
});
|
|
if(scaleType !== "linear"){
|
|
linlog.innerHTML = "<strong>☒</strong> log";
|
|
}
|
|
|
|
let tbl = document.createElement('table');
|
|
legend.appendChild(tbl);
|
|
let legendbody = document.createElement('tbody')
|
|
tbl.appendChild(legendbody);
|
|
let legendvalues = {};
|
|
let legendlabels = {};
|
|
let legendlines = {};
|
|
|
|
let startX=0,startY=0, startElX=0, startElY=0,legendmoving=false;
|
|
|
|
/**
|
|
* Position the legend window at the given x and y (potentially adjusted)
|
|
* @param {number} x - The x position of the top left hand corner
|
|
* @param {number} y - The y position of the top left hand corner
|
|
* @returns If the legend cannot be displayed
|
|
*/
|
|
function positionLegend(x, y) {
|
|
let lrect = legend.getBoundingClientRect();
|
|
let prect = parent.getBoundingClientRect();
|
|
ys = 22;
|
|
|
|
x = Math.max(0, Math.min(x, prect.width - lrect.width));
|
|
legend.style.left = x + "px";
|
|
|
|
/*
|
|
let lim = prect.height - lrect.height;
|
|
Y = Math.max(Math.min(0, lim), Y);
|
|
Y = Math.min(Math.max(lim, 0), Y);
|
|
*/
|
|
y = Math.max(ys-lrect.height, Math.min(prect.height-ys, y));
|
|
legend.style.top = y + "px";
|
|
|
|
let mid = y + lrect.height / 2;
|
|
if (mid >= 0 && mid <= prect.height) {
|
|
legendAnchor.style.display = 'none';
|
|
hasLegendAnchor = false;
|
|
return;
|
|
}
|
|
if (y < 0) {
|
|
y = Math.min((prect.height - ys) / 2, y + lrect.height - ys);
|
|
} else {
|
|
y = Math.max((prect.height - ys) / 2, y);
|
|
}
|
|
legendAnchor.style.display = 'inline-block';
|
|
legendAnchor.style.left = x + lrect.width + 'px';
|
|
legendAnchor.style.top = y + 'px';
|
|
hasLegendAnchor = true;
|
|
}
|
|
|
|
/**
|
|
* The callback called if the legend is moving. Sets the position of the legend at the mouse position.
|
|
* @param {*} e The JS event
|
|
* @return If the mouse does not move enough
|
|
*/
|
|
function legendmousemove(e){
|
|
if (Math.abs(e.pageX-startX) + Math.abs(e.pageY-startY) < 4) return;
|
|
legendmoving = true;
|
|
let x = startElX + (e.pageX-startX), y = startElY + (e.pageY-startY);
|
|
positionLegend(x, y);
|
|
}
|
|
|
|
/**
|
|
* Removes the different listeners on the legend when the mouse press is finished (within 200ms)
|
|
* @param {*} e - The JS event
|
|
*/
|
|
function legendmouseup(e){
|
|
setTimeout(function(){
|
|
legendmoving=false;
|
|
}, 200);
|
|
window.removeEventListener('mousemove', legendmousemove);
|
|
window.removeEventListener('mouseup', legendmouseup);
|
|
window.removeEventListener('blur', legendmouseup);
|
|
}
|
|
|
|
// Adds the different event listeners on the legend when it is clicked
|
|
legend.addEventListener('mousedown', function(e){
|
|
if(e.which !== 1){
|
|
return;
|
|
}
|
|
//graphs.bringToFront(legend);
|
|
startX = e.pageX;
|
|
startY = e.pageY;
|
|
startElX = legend.offsetLeft;
|
|
startElY = legend.offsetTop;
|
|
window.addEventListener('mousemove', legendmousemove);
|
|
window.addEventListener('mouseup', legendmouseup);
|
|
window.addEventListener('blur', legendmouseup);
|
|
})
|
|
|
|
// Brings the legend to the front is the mouse hovers it
|
|
legend.addEventListener('mouseover', function(e){
|
|
graphs.bringToFront(legend);
|
|
});
|
|
|
|
legend.style.display = 'none';
|
|
|
|
let margin = 10;
|
|
/*
|
|
function clickHandler(e){
|
|
let trect = e.target.getBoundingClientRect();
|
|
let X = e.clientX - trect.x, Y = e.clientY - trect.y;
|
|
graphs.showLegends(true, false);
|
|
graphs.setLiveMode();
|
|
graphs.cursorLine(X);
|
|
graphs.update();
|
|
}
|
|
*/
|
|
//canvas.addEventListener('click', clickHandler)
|
|
|
|
canvas.addEventListener('mouseover', function(e){
|
|
graphs.bringToFront(legend);
|
|
});
|
|
|
|
/**
|
|
* Shows the legend or hide it with the cursor
|
|
* @param {*} flag - Tells if we need to show or note (+ cursor)
|
|
*/
|
|
function showLegend(flag){
|
|
if (flag) {
|
|
legend.style.display = 'flex';
|
|
if (hasLegendAnchor)
|
|
legendAnchor.style.display = 'inline-block';
|
|
} else {
|
|
graphs.cursorLine(null);
|
|
legend.style.display = 'none';
|
|
if (hasLegendAnchor)
|
|
legendAnchor.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the zoom mode of the current graph
|
|
* @param {string} to - The zoom mode to set
|
|
*/
|
|
function setZoomMode(to){
|
|
chart.options.zoom.mode = to;
|
|
}
|
|
|
|
// Unused
|
|
function setPanOnOff(to){
|
|
chart.options.pan.enabled = to;
|
|
}
|
|
|
|
// Unused
|
|
function addTime(data){
|
|
console.log('OBSOLETE addTime');
|
|
chart.data.labels = data
|
|
chart.update();
|
|
}
|
|
|
|
/**
|
|
* Adds a dataset (curve) to the current graph, and adds its legend entry in the graph legend
|
|
* @param {string} key - The curve name (variable)
|
|
* @param {*} data - The corresponding data points for this curve
|
|
* @param {*} opts - Some options for the curve (color, label)
|
|
* @returns The index of this curve in its chart
|
|
*/
|
|
function addDataset(key, data, opts){
|
|
let dataset_index = chart.data.datasets.length;
|
|
chart.data.datasets.push({data: data, label: opts.label, key: key,
|
|
spanGaps: false, lineJoin: 'round', borderWidth: 2, steppedLine: opts.period == 0,
|
|
borderColor: opts.color,fill: false, pointRadius: 0, tension:0, showLine: true});
|
|
|
|
let dataset = chart.data.datasets[dataset_index];
|
|
let legendrow = document.createElement('tr');
|
|
legendrow.classList.add('legendrow')
|
|
|
|
let color = document.createElement('td');
|
|
color.classList.add('semitransparent');
|
|
let dlabel = document.createElement('td');
|
|
dlabel.classList.add('semitransparent');
|
|
let tdvalue = document.createElement('td');
|
|
|
|
legendrow.appendChild(color);
|
|
legendrow.appendChild(dlabel);
|
|
legendrow.appendChild(tdvalue);
|
|
|
|
let colorline = document.createElement('div');
|
|
color.appendChild(colorline);
|
|
colorline.classList.add('colorline');
|
|
colorline.style.backgroundColor = dataset.borderColor;
|
|
|
|
dlabel.innerHTML = dataset.label;
|
|
//dlabel.addEventListener('click', function(evt){
|
|
// /* dummy listener. really needed ? */
|
|
//});
|
|
|
|
let dvalue = document.createElement('div');
|
|
dvalue.classList.add('value');
|
|
tdvalue.appendChild(dvalue);
|
|
|
|
legendlines[key] = colorline;
|
|
legendvalues[key] = dvalue;
|
|
legendlabels[key] = dlabel;
|
|
legendrow.addEventListener('click', function(evt){
|
|
if (legendmoving) return;
|
|
for (let k in legendlabels) {
|
|
// set all labels to normal font
|
|
legendlabels[k].style.fontWeight = 400;
|
|
}
|
|
if (evt.target == dlabel) {
|
|
// disable all
|
|
for (let k in legendlines) {
|
|
legendlines[k].style.height = '1px';
|
|
}
|
|
for (ds of chart.data.datasets) {
|
|
ds.borderWidth = 1;
|
|
}
|
|
colorline.style.height = '2px';
|
|
dataset.borderWidth = 2;
|
|
dlabel.style.fontWeight = 700; // bold
|
|
} else {
|
|
if (dataset.borderWidth == 1) {
|
|
colorline.style.height = '2px';
|
|
dataset.borderWidth = 2;
|
|
} else {
|
|
colorline.style.height = '1px';
|
|
dataset.borderWidth = 1;
|
|
allDeselected = true;
|
|
for (ds of chart.data.datasets) {
|
|
if (ds.borderWidth != 1) allDeselected = false;
|
|
}
|
|
if (allDeselected) {
|
|
for (ds of chart.data.datasets) {
|
|
ds.borderWidth = 2;
|
|
}
|
|
for (let k in legendlines) {
|
|
legendlines[k].style.height = '2px';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
console.log('AUTO')
|
|
graphs.autoScale(chart);
|
|
update();
|
|
});
|
|
legendbody.appendChild(legendrow);
|
|
return dataset_index;
|
|
}
|
|
|
|
/**
|
|
* Autoscale the graphs or reset the autoscale button of each graph
|
|
* @param {boolean} clear - Tells if the autoscale button has to be reset
|
|
*/
|
|
function autoScaleIf(clear=false) {
|
|
if (clear) setAutoScale(false);
|
|
if (autoScaleFlag) graphs.autoScale(chart);
|
|
}
|
|
|
|
/**
|
|
* Appends the data_point at the end of the dataset at the given dataset_index in the current Graph
|
|
* @param {number} dataset_index - The index of the dataset (curve) in the current Graph
|
|
* @param {{x: number, y: number}} data_point - The new data point to append
|
|
*/
|
|
function pushData(dataset_index, data_point){
|
|
data = chart.data.datasets[dataset_index].data;
|
|
//if (chart.data.datasets[dataset_index].key == 'tt:target')
|
|
// console.log('BEFORE', data.slice(-3))
|
|
if (data.slice(-1)[0] && data.slice(-1)[0].x >= data_point.x) {
|
|
// replace the last point, when x is lower. this is due to the artifical point added at the end by the live reader
|
|
removed = data.pop();
|
|
}
|
|
data.push(data_point);
|
|
}
|
|
|
|
/**
|
|
* Sets (overwrites) the data for a curve (variable)
|
|
* @param {*} index - The index of the curve (variable) in its graph object (Chart)
|
|
* @param {*} data - The data to set at the given index curve
|
|
*/
|
|
function setData(index, data){
|
|
chart.data.datasets[index].data = data;
|
|
}
|
|
|
|
/**
|
|
* Sets the current viewing window (axis) to min and max (for x values)
|
|
* @param {number} min - The minimum timestamp in milliseconds of the viewing window
|
|
* @param {number} max - The maximum timestamp in milliseconds of the viewing window
|
|
*/
|
|
function setMinMax(min, max){
|
|
let ax = chart.options.scales.xAxes[0];
|
|
let ay = chart.options.scales.yAxes[0];
|
|
// clamp X-span
|
|
let span = max - min;
|
|
let half = 0;
|
|
if (chart.lastXmin) {
|
|
if (span > maxspan) {
|
|
half = maxspan * 0.5;
|
|
} else if (span < minspan) {
|
|
half = minspan * 0.5;
|
|
}
|
|
}
|
|
if (half) { // clamped
|
|
mid = (chart.lastXmin + chart.lastXmax) * 0.5;
|
|
min = mid - half;
|
|
max = mid + half;
|
|
ay.ticks.min = chart.lastYmin;
|
|
ay.ticks.max = chart.lastYmax;
|
|
} else {
|
|
chart.lastXmin = min;
|
|
chart.lastXmax = max;
|
|
chart.lastYmin = ay.ticks.min;
|
|
chart.lastYmax = ay.ticks.max;
|
|
}
|
|
// custom algorithm for tick step
|
|
mainstep = 1000;
|
|
step = 1000;
|
|
for (info of [['second', 60, [1, 2, 5, 10, 15, 30]],
|
|
['minute', 60, [1, 2, 5, 10, 15, 30]],
|
|
['hour', 24, [1, 2, 4, 6, 12]],
|
|
['day', 365, [1,2,4,7,14,31]]]) {
|
|
if (span < 12 * mainstep * info[2].slice(-1)[0]) {
|
|
for (fact of info[2]) {
|
|
step = mainstep * fact;
|
|
if (span < 12 * step) {
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
mainstep *= info[1];
|
|
}
|
|
ax.time.unit = info[0];
|
|
ax.time.stepSize = Math.round(step / mainstep);
|
|
//ax.ticks.unit = ax.time.unit;
|
|
//ax.ticks.stepSize =ax.time.stepSize;
|
|
//console.log('INFO', step, mainstep, info, ax, ax.time);
|
|
ax.ticks.max = max;
|
|
ax.ticks.min = min;
|
|
}
|
|
|
|
/**
|
|
* Triggers the autocale of all the Graphs, then applies the changes to them, and finally updates the HTML autoscale button
|
|
* @param {boolean} flag - Tells if the autoscale has been set or not
|
|
*/
|
|
function setAutoScale(flag) {
|
|
autoScaleFlag = flag;
|
|
if (autoScaleFlag) {
|
|
graphs.autoScale(chart);
|
|
update();
|
|
autoScaleRow.innerHTML = "<strong>☒</strong> autoscale";
|
|
} else {
|
|
autoScaleRow.innerHTML = "<strong>☐</strong> autoscale";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inverts the type of the y-axis (linear <-> logarithmic)
|
|
* Called when log button in the legend is clicked
|
|
*/
|
|
function toggleAxesType(){
|
|
setAxesType((chart.options.scales.yAxes[0].type=== 'linear') ? 'logarithmic' : 'linear');
|
|
}
|
|
|
|
/**
|
|
* Sets the y-axis type with a 800ms animation, then applies the changes
|
|
* @param {string} type - The axis type to set
|
|
*/
|
|
function setAxesType(type){
|
|
scaleType = type;
|
|
if(type == "linear"){
|
|
linlog.innerHTML = "<strong>☐</strong> log";
|
|
}else{
|
|
linlog.innerHTML = "<strong>☒</strong> log";
|
|
}
|
|
chart.options.scales.yAxes[0].type = type;
|
|
chart.options.animation.duration = 800;
|
|
if (autoScaleFlag) graphs.autoScale(chart);
|
|
update();
|
|
setTimeout(function(){chart.options.animation.duration = 0;},850)
|
|
}
|
|
|
|
/**
|
|
* Updates the single value point in the legend for each curve in each graph based on the position of the cursor.
|
|
* @returns if there is no cursor (so nothing to show)
|
|
*/
|
|
function redraw(){
|
|
x = graphs.cursorLine(canvas.clientWidth, true);
|
|
if (x === null) return;
|
|
for(let i in chart.data.datasets){
|
|
let y = null;
|
|
for(let j = 0; j < chart.getDatasetMeta(i).data.length; j++){
|
|
let dp = chart.getDatasetMeta(i).data[j];
|
|
if (dp._model.x >= x) break;
|
|
y = chart.data.datasets[i].data[dp._index].y;
|
|
}
|
|
valueElm = legendvalues[chart.data.datasets[i].key];
|
|
if (labelMinWidth == 0) {
|
|
valueElm.style.minWidth = '0px';
|
|
valueElm.innerHTML = labelLongValue;
|
|
labelMinWidth = valueElm.clientWidth;
|
|
valueElm.style.minWidth = labelMinWidth + 'px';
|
|
}
|
|
if (y !== null) {
|
|
valueElm.innerHTML = strFormat(y, labelDigits);
|
|
}
|
|
}
|
|
|
|
/*
|
|
//console.log('REDRAW', dselect.innerHTML, x);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, canvas.height);
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "rgba(0,0,0,0.8)";
|
|
//ctx.globalCompositeOperation = "destination-over";
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.stroke();
|
|
ctx.globalCompositeOperation = "source-over";
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* Applies the changes to the chart object and redraws the legend (single value)
|
|
*/
|
|
function update(){
|
|
chart.update();
|
|
redraw();
|
|
}
|
|
/**
|
|
* Sets the digits of the legend labels depending on the extremas of the curves for this graph
|
|
* @param {number} min - The mimimum y-value of all the curves of the graph
|
|
* @param {number} max - The maximum y-value of all the curves of the graph
|
|
*/
|
|
function setLabelDigits(min, max) {
|
|
let dig = parseInt(Math.max(-min, max) / (max - min)).toFixed(0).length + 3;
|
|
let l = 0;
|
|
for (let val of [min, max]) {
|
|
evalue = val.toExponential(dig - 1).replace("e+", "e");
|
|
fvalue = val.toFixed(Math.max(0, dig - Math.abs(parseInt(val)).toFixed().length));
|
|
if (evalue.length < fvalue.length) fvalue = evalue;
|
|
if (fvalue.length > l) {
|
|
l = fvalue.length;
|
|
labelMinWidth = 0;
|
|
labelLongValue = fvalue;
|
|
}
|
|
}
|
|
labelDigits = dig;
|
|
}
|
|
|
|
self = {
|
|
addTime: addTime,
|
|
addDataset: addDataset,
|
|
pushData: pushData,
|
|
setMinMax: setMinMax,
|
|
setAxesType: setAxesType,
|
|
/* clickHandler: clickHandler, */
|
|
setZoomMode: setZoomMode,
|
|
showLegend: showLegend,
|
|
setPanOnOff:setPanOnOff,
|
|
redraw: redraw,
|
|
update: update,
|
|
setData: setData,
|
|
autoScaleIf: autoScaleIf,
|
|
setAutoScale: setAutoScale,
|
|
positionLegend: positionLegend,
|
|
setLabelDigits: setLabelDigits,
|
|
chart: chart,
|
|
legend: legend,
|
|
}
|
|
|
|
chart.graph = self;
|
|
return self;
|
|
}
|
|
|
|
/**
|
|
* Called when new data is received from the server (SSE).
|
|
* Appends new data to the corresponding curves' datasets
|
|
* @param {{type:"graph-update", reduced:boolean, time:number, graph:{string:[[(number),(number)]]}}} graph - The graph response from the server
|
|
*/
|
|
function updateCharts2(graph){
|
|
if(!graphs.doUpdates()) {
|
|
console.log('graphs.doUpdates skipped');
|
|
return;
|
|
}
|
|
for(let key in graph){
|
|
if (graph[key][0] != null) {
|
|
// there is at least ONE valid datapoint
|
|
for (pt of graph[key]) {
|
|
graphs.newDataHandler(key, {x: pt[0]*1000, y: pt[1]});
|
|
}
|
|
}
|
|
}
|
|
graphs.updateAuto(false);
|
|
// graphs.update();
|
|
}
|
|
|
|
/** Useless (called in graph-draw response case in SEAWebClientCommunication) */
|
|
function createCharts2(arg) {
|
|
console.log('C2', arg)
|
|
}
|