// 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)); } }); }); } // arguments are passed like param1=value1¶m2=value2... this.postForm = function(args){ xhr.open("POST", addr, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.send(args); 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"; /** * Function used to handle some global controls for the graphs */ 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_wide.png", "res/y_zoom_white_wide.png", "Time<->Y zoom (one graph)", graphs.toggleZoomMode, graphs.toggleZoomMode); let xyControl = new Control("res/icon_width.png", "res/icon_height.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 datesPopup = undefined; /** * Used to add the Dates Popup (initialization + adding it to the DOM) */ function loadDatesPopup(){ let graphsContainer = document.getElementsByClassName("graphs-container")[0]; datesPopup = new DatesPopup(graphs.gotoNow, graphs.jumpToDate); graphsContainer.appendChild(datesPopup); } // Defining keys for global indicators let datesKey = "dates-indicator"; /** * Function used to handle some global indicators for the graphs */ let globalIndicators = (function (){ let indicatorsMap = {} function loadIndicators(panel){ let leftDate = Date.now() - 30*60*1000; let datesIndicator = new DateIndicator(leftDate); datesIndicator.addEventListener("click", function () { exportPopup.hide(); curvesSettingsPopup.hide(); menuGraphicsPopup.hide(); datesPopup.show(); }) panel.appendChild(datesIndicator); datesIndicator.style.marginLeft = "auto"; datesIndicator.style.marginRight = "auto"; indicatorsMap[datesKey] = datesIndicator; } function getIndicatorsMap(){ return indicatorsMap; } return { loadIndicators: loadIndicators, getIndicatorsMap: getIndicatorsMap } })() let menuGraphicsPopup = undefined; /** * Defines all the entries in the menu and adds it to the DOM * @param {*} panel The panel on which to add the menu icon */ function loadGraphicsMenu(panel){ menuGraphicsPopup = new MenuPopup(); let exportActionEntry = new ActionEntry("Export", graphs.displayExportPopup, () => {menuGraphicsPopup.hide()}); let curvesSettingsActionEntry = new ActionEntry("Curves settings", () => {curvesSettingsPopup.show();}, () => {menuGraphicsPopup.hide()}); let hideShowInstrumentDevicesHelpEntry = new HelpEntry("Hide/show instrument and devices", "If the 'instrument and devices' label is taking too much space, you can click on it (not on the house at the left) to hide it. " + "You can then click once again on the left gray square to make it appear again." ); let toggleZoomModeHelpEntry = new HelpEntry("Toggle the zoom mode", "On the top curves bar, a horizontal double arrow means that a zoom gesture will trigger a zoom on the time axis of all the graphs. " + "Clicking on it will change the zoom mode to the y direction. If you perform a zoom gesture on a graph, it will zoom in the y direction while disabling the autoscale for this graph. " + "You can go back to the time zoom mode by clicking again on the arrow." ) let hideShowCursorHelpEntry = new HelpEntry("Hide/show the cursor", "To show the cursor, you can click on any graph. To hide it, you can double click/tap on any graph."); let hideShowLegendsHelpEntry = new HelpEntry("Hide/showe the legends", "You can remove all legends by clicking on the cross of one of them. They will appear again if you click on any graph.") let showMoreCurvesHelpEntry = new HelpEntry("Show more curves", "On each graph, you can click on the unit at the top right hand corner to open a selection window. There, you can choose which block you want to display. " + "You can also add curves (especially parameters) with the curves settings." ); let historicalDataHelpEntry = new HelpEntry("Look for historical data", "Clicking on the date displayed at the top of the curves opens a popup."+ "You can then select the date you want to jump to, and the time at which you want the available curves. You can also go to now. The date displayed will be the one corresponding " + "to the lower bound of the viewing window. On wide screens, a jump will close the right part, and going to now will display it back."); let aboutCurvesSettingsHelpEntry = new HelpEntry("About curves settings", "To indicate a variable, you need to type the SECOP module name (case sensitive) of the variable you want to act on. " + "Leaving the parameter empty means that you will act on the value parameter (but you can still write 'value'). For the category, '-' means that this curve will be hidden. To display a curve which is not displayed yet, the category has to be filled. " + "The character '*' means that the curve will be displayed in the block of its unit. If one of the fields are filled (for the category, other than '*') but not the variable, an error will be thrown. " + "If nothing is set for the category (except '*'), the color and the unit, the row will be ignored. Closing the popup via the cross or the 'Apply' button will save your configuration and reload the curves. " + "You can discard your changes via the 'Cancel' button.") let aboutTopRightHandCornerCrossHelpEntry = new HelpEntry("About top right hand corner cross", "On wide screens, this cross toggles the right part visibility when control is enabled for this instrument. " + "On small screens (mobile phones for example), it allows you to switch to the next section of the app (from graphs block to main block to console block)." ); menuGraphicsPopup.addEntry(exportActionEntry) menuGraphicsPopup.addEntry(curvesSettingsActionEntry); menuGraphicsPopup.addHorizontalDivider(); menuGraphicsPopup.addEntry(hideShowInstrumentDevicesHelpEntry); menuGraphicsPopup.addEntry(toggleZoomModeHelpEntry); menuGraphicsPopup.addEntry(hideShowCursorHelpEntry); menuGraphicsPopup.addEntry(hideShowLegendsHelpEntry); menuGraphicsPopup.addEntry(showMoreCurvesHelpEntry); menuGraphicsPopup.addEntry(historicalDataHelpEntry); menuGraphicsPopup.addEntry(aboutCurvesSettingsHelpEntry); menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry); // let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => { let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "res/icon_menu_graphics.png", "Menu", () => { datesPopup.hide(); exportPopup.hide(); curvesSettingsPopup.hide(); menuGraphicsPopup.show(); }); panel.appendChild(menuGraphicsPopup); menuGraphicsPopup.getContainer().style.top = "28px"; menuGraphicsPopup.getContainer().style.right = "20px"; menuGraphicsPopup.style.position = "absolute"; panel.appendChild(graphicsMenuControl); graphicsMenuControl.style.marginLeft="0px"; graphicsMenuControl.style.marginRight="8px"; graphicsMenuControl.style.marginTop="2px"; } let exportPopup = undefined; /** * Used to add the export Popup (initialization + adding it to the DOM) */ function loadExportPopup(){ let graphsContainer = document.getElementsByClassName("graphs-container")[0]; exportPopup = new ExportPopup(exportCallback); graphsContainer.appendChild(exportPopup); } /** * Function call when the Export button is clicked in the Export popup */ function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){ if (binning === null || binning == "None") binning = ""; let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binning + "&id=" + clientID let a = document.createElement('a'); a.href = exportURL a.download = true a.click() } let curvesSettingsPopup = undefined; /** * Used to add the curves settings Popup (initialization + adding it to the DOM) */ function loadCurvesSettingsPopup(){ let graphsContainer = document.getElementsByClassName("graphs-container")[0]; curvesSettingsPopup = new CurvesSettingsPopup(graphs.applySettingsCallback); graphsContainer.appendChild(curvesSettingsPopup); } 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; let 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 clickMode = 0; // 1: mouse is down, 2: pan is active, 0: after mouse down 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 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; // current window resolution (ms/pixel) let activateUpdateTimeout = undefined; // timeout for the activateUpdates function let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback) 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}]}} 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]} 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]; resolution = getResolution((maxTime - minTime) / 1000) 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]}); } addDataset(gindex, key, pdata, dict[key]) // 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) { ay = chart.options.scales.y; ax = chart.options.scales.x; 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, ax.min, ax.max); let lmin = minAr(ds.data, ax.min, ax.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 >= ay.min && ay.min >= extraMin && max <= ay.max && ay.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; } ay.min = min; ay.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; if (X == cursorLinePos) { cursorLine(null); } else { menuGraphicsPopup.hide(); 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); */ } } } function mouseDown(evt) { clickMode = 1; } function mouseUp(evt) { if (clickMode == 1) { // mouse was down, but no pan happend clickHandler(evt); } clickMode = 0; } // container.addEventListener('click', clickHandler) container.addEventListener('mousedown', mouseDown); container.addEventListener('mouseup', mouseUp); /** * 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; } resolution = getResolution((currentMaxTime - currentMinTime) / 1000) 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 ax = graph.chart.options.scales.x; let xmin = ax.min, xmax = ax.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 a, min, max; if (currentZoomMode == 'y') { a = graph.chart.options.scales.y; } else { a = graph.chart.options.scales.x; } min = a.min; max = a.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') { a.min = min; a.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 getResolution(timeDelta){ return Math.ceil((timeDelta / container.getBoundingClientRect().width)) } /** * The callback to be called when the user click on the "Jump" button of the date selector * Gets the vars + device name for the selected date+time, then rebuilds the graphs * @param {*} dateTimestampMs The user input for the date in ms (Unix timestamp) * @param {*} timeValueMs The user input for the time in ms */ function jumpToDate(dateTimestampMs, timeValueMs){ cursorLine(null); window["wideGraphs"] = true; adjustGrid() let msLeftTimestampGetGraph = 0 , msRightTimestampGetVars = 0, msRightTimestampGetGraph = 0; msLeftTimestampGetGraph = dateTimestampMs; msRightTimestampGetVars = dateTimestampMs + timeValueMs; msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000; AJAX("http://" + hostPort + "/getvars").postForm( "time=" + msRightTimestampGetVars/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){ blocks = data.blocks; document.getElementById("device").innerHTML = data.device maxTime = msRightTimestampGetGraph; minTime = msLeftTimestampGetGraph; currentMaxTime = maxTime; currentMinTime = minTime; globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime); ngraphs = 0; // forces all the graphs to reset createGraphs(); if (liveMode && msRightTimestampGetGraph < lastTime) setLiveMode(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 ax = graph.chart.options.scales.x; let xmin = ax.min, xmax = ax.max; clickMode = 2; // mouse pan mode 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 and device name for the visualization window of [now-30min, now]) then rebuilds the graphs */ function gotoNow() { let msRightTimestamp = graphs.now(); let msLeftTimestamp = msRightTimestamp - 30*60*1000; cursorLine(null); window["wideGraphs"] = false; // will have no effect if hideRightPart is true adjustGrid(); AJAX("http://" + hostPort + "/getvars").postForm( "time=" + msRightTimestamp/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){ currentMaxTime = msRightTimestamp + 60000; currentMinTime = msLeftTimestamp; maxTime = msRightTimestamp; minTime = msLeftTimestamp; blocks = data.blocks; document.getElementById("device").innerHTML = data.device ngraphs = 0; createGraphs(); globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime); }); } /** * Builds the main container for the graphs by inserting a slide, and builds the top panel */ function buildGraphicsUI(){ let f = 0; appendToGridElement(f, " ", "graphics", container); let graphicsPanel = container.parentNode.querySelector('.panel') graphicsPanel.classList.add('graphics'); graphicsPanel.childNodes[0].style.visibility = "hidden"; // hides the span added by the swippers loadExportPopup(); loadCurvesSettingsPopup(); loadDatesPopup(); globalIndicators.loadIndicators(graphicsPanel); globalControls.loadControls(graphicsPanel); loadGraphicsMenu(graphicsPanel); if (isTouchDevice) { doubleTap(removeCursor); } else { window.addEventListener('dblclick', removeCursor); showLegends(true, false); adjustLegends(); } } /** * Inits the graph content * @param {[{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}]} blocks - the received blocks */ function initGraphs(blocks){ buildGraphicsUI(); receivedVars(blocks); } /** * Holds the received variables from the /getvars call, gets the server time, then creates the graphs * @param {[{tag:string, unit:string, curves:[{name:string, label:string, color:string}]}]} blocks_arg - the received blocks */ function receivedVars(blocks_arg){ maxTime = timeRange[1]*1000; minTime = timeRange[0]*1000; if (currentMaxTime == 0) { currentMaxTime = maxTime; currentMinTime = minTime; } AJAX("http://" + hostPort + "/gettime?time=" + window['timerange'] + "&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(); }); } // Changes the zoom mode icon depending on the current zoom mode function setInnerZoomMode(){ if (currentZoomMode == 'y') { globalControls.getControlsMap()[xyKey].changeToAlt(); } else { globalControls.getControlsMap()[xyKey].changeToMain(); } prevTime = null; // reset zoom speed time } // Toggles the current zoom mode and sets it for each graph 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.min(blocks.length,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 displayExportPopup(){ exportPopup.show(blocks,currentMinTime, currentMaxTime); } /** * Function called when the user configuration needs to be applied. * Calls the /getvars routes with the userconfiguration and reloads all the graphics * @param {*} userConfiguration The JSON object representing the user configuration */ function applySettingsCallback(userConfiguration){ cursorLine(null); AJAX("http://" + hostPort + "/getvars").postForm( "time=" + currentMaxTime/1000 + "&userconfiguration=" + JSON.stringify(userConfiguration) + "&id="+ clientID).then(function(data){ blocks = data.blocks; document.getElementById("device").innerHTML = data.device maxTime = currentMaxTime; minTime = currentMinTime; globalIndicators.getIndicatorsMap()[datesKey].update(currentMinTime); ngraphs = 0; // forces all the graphs to reset createGraphs(); if (liveMode && currentMaxTime < lastTime) setLiveMode(false); }); } /** * Make sure that the updatAuto() function called in chartJS' onZoomComplete callback is delayed * This is needed for mobile phones, as this callback is triggered too often */ 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, displayExportPopup:displayExportPopup, applySettingsCallback:applySettingsCallback } })(); 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; let chart_options = { responsive: true, maintainAspectRatio: false, animation:{duration:0}, scales: { y:{ beginAtZero: false, ticks:{ mirror: true, padding: -10, callback: function(label, index, labels) { if(index == 0 || index == labels.length-1) return ""; return strFormat(label); } }, grid:{drawTicks:false}, title: false, //Former scaleLabel type: scaleType, position: 'right', afterBuildTicks: function(axis) { let ticks = 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 }, }, x:{ title: false, // former scaleLabel type: 'time', time: { displayFormats: {'millisecond': 'HH:mm:ss.SSS', 'second': 'HH:mm:ss', 'minute': 'HH:mm','hour': 'EEE d. HH:mm', 'day': 'EE d.', 'week': 'd. MMM yy', 'month': 'MMM yy'}, }, ticks: { padding: -20, // stepSize: 180000, autoSkip: true, maxRotation: 0, // callback not used, this is better done in afterBuildTicks }, afterBuildTicks: function(axis) { let ticks = 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 result = []; let v = axis.min; for (tick of ticks) { v = tick.value - offset; if (v > axis.min + step / 2) { result.push({value: v, major: false}); } } v += step; if (v < axis.max) result.push({value:v, major: false}); axis.ticks = result; // return result; }, beforeFit: function(axis) { // called after ticks are autoskipped prevday = ''; for (tick of axis.ticks) { s = tick.label.split(' '); if (s.length == 3) { // format with day // show date only on first label of a day day = s.slice(0, 2).join(' '); if (day != prevday) { prevday = day; } else { tick.label = s[2]; // time } } } }, grid:{drawTicks:false}, } }, plugins:{ tooltip: false, legend: false, zoom:{ pan: { enabled: true, mode: 'xy', speed: 10, threshold: 10, onPan: function({chart}) { graphs.panCallback(chart.graph);}, onPanComplete: function({chart}){graphs.updateAuto();}, }, zoom: { wheel:{ enabled: true }, pinch:{ enabled: true }, mode: isTouchDevice ? 'xy': 'x', speed: 0.1, sensitivity: 1, onZoom: function({chart}) { graphs.zoomCallback(chart.graph);}, onZoomComplete: function({chart}){graphs.onZoomCompleteCallback()}, } } } } if (gindex != 0) { // show time labels only on first chart chart_options.scales.x.ticks.callback = function () { return ' '; } } chart = new Chart(ctx, {type: 'scatter', options: chart_options}) //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; } 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); 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'; 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; let linewidth = 3; 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.plugins.zoom.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, borderJoinStyle: 'bevel', borderWidth: linewidth, stepped: 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; colorline.style.height = linewidth + 'px'; 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 = linewidth + 'px'; dataset.borderWidth = linewidth; dlabel.style.fontWeight = 700; // bold } else { if (dataset.borderWidth == 1) { colorline.style.height = linewidth + 'px'; dataset.borderWidth = linewidth; } 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 = linewidth; } for (let k in legendlines) { legendlines[k].style.height = linewidth + 'px'; } } } } 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.x; let ay = chart.options.scales.y; // 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.min = chart.lastYmin; ay.max = chart.lastYmax; } else { chart.lastXmin = min; chart.lastXmax = max; chart.lastYmin = ay.min; chart.lastYmax = ay.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]; } unit = info[0]; rstep = Math.round(step / mainstep); ax.time.unit = unit; ax.ticks.stepSize = rstep; ax.max = max; ax.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.y.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.y.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; let metadata = chart.getDatasetMeta(i).data; let dataset = chart.data.datasets[i]; if (metadata.length != dataset.data.length) { console.log('length mismatch in dataset.data and metadata') } for(let j = 0; j < metadata.length; j++){ let dp = metadata[j]; if (dp.x >= x) break; y = dataset.data[j].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 = ''; } else { 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, 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", 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; } //console.log('G', graph); 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) }