Files
seweb/client/jsFiles/SEAWebClientGraphics.js

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 = "&#215;";
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 = '&#215;';
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>&#9746;</strong> autoscale", function(){
setAutoScale(!autoScaleFlag);
});
let linlog = addControl("<strong>&#9744;</strong> log", function(e){
//graphs.toggleAxesType();
toggleAxesType();
});
if(scaleType !== "linear"){
linlog.innerHTML = "<strong>&#9746;</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>&#9746;</strong> autoscale";
} else {
autoScaleRow.innerHTML = "<strong>&#9744;</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>&#9744;</strong> log";
}else{
linlog.innerHTML = "<strong>&#9746;</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)
}