// Graph /* interface: updateCharts2(graph) graph is a dict 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]; console.log("clear for createSelection", gindex) clear(gindex); let selection = document.createElement('div'); selection.classList.add('selection'); delete prev_blk[gindex]; console.log('cresel', gindex, prev_blk, tag_dict); let creidx = null; let creblock = null; for (let i in prev_blk) { creblock = prev_blk[i]; if (tag_dict[creblock.tag] == gindex) { creidx = i; break; } } for (let block of blocks) { console.log('ck', block.tag, tag_dict[block.tag], creidx) 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) { let idx = tag_dict[block.tag]; createSelection(idx); prev_blk[idx] = block; } createGraph(gindex, block); }) selection.appendChild(bel); } graph_elm.appendChild(selection); if (creidx !== null) { console.log('creblock', creidx, creblock); createGraph(creidx, creblock); } } /** * 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); //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(" autoscale", function(){ setAutoScale(!autoScaleFlag); }); let linlog = addControl(" log", function(e){ //graphs.toggleAxesType(); toggleAxesType(); }); if(scaleType !== "linear"){ linlog.innerHTML = " 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'; 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'; } /** * 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'; } else { graphs.cursorLine(null); legend.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 = " autoscale"; } else { autoScaleRow.innerHTML = " 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 = " log"; }else{ linlog.innerHTML = " 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) }