3// Created by: Stefan Ritt
5// Contents: JavaScript history plotting routines
7// Note: please load midas.js, mhttpd.js and control.js before mhistory.js
14function log_hs_read(str, a, b)
16 let da = new Date(a*1000);
17 let db = new Date(b*1000);
19 da.toLocaleDateString() +
21 da.toLocaleTimeString() +
23 db.toLocaleDateString() +
25 db.toLocaleTimeString();
27 // out-comment following line to log all history requests
31function profile(flag) {
32 if (flag === true || flag === undefined) {
34 profile.startTime = new Date().getTime();
38 let now = new Date().getTime();
39 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
40 profile.startTime = new Date().getTime();
43function mhistory_init(mhist, noBorder, param) {
44 // go through all data-name="mhistory" tags if not passed
46 if (mhist === undefined) {
47 var mhist = document.getElementsByClassName("mjshistory");
48 } else if (!Array.isArray(mhist)) {
53 let baseURL = window.location.href;
54 if (baseURL.indexOf("?cmd") > 0)
55 baseURL = baseURL.substring(0, baseURL.indexOf("?cmd"));
56 baseURL += "?cmd=history";
58 for (let i = 0; i < mhist.length; i++) {
59 mhist[i].innerHTML = ""; // Needed to make sure of a fresh start
60 mhist[i].dataset.baseURL = baseURL;
61 mhist[i].mhg = new MhistoryGraph(mhist[i], noBorder, floating);
62 mhist[i].mhg.initializePanel(i, param);
63 mhist[i].mhg.resize();
64 mhist[i].resize = function () {
70function mhistory_dialog_var(historyVar, param) {
71 if (param === undefined)
72 mhistory_dialog(undefined, historyVar);
74 mhistory_dialog(undefined, historyVar, param.width, param.height, param.x, param.y, param);
78function mhistory_dialog(group, panel, width, height, x, y, param) {
80 // default minimal/initial width and height if not defined
81 if (width === undefined)
83 if (height === undefined)
86 let d = document.createElement("div");
87 d.className = "dlgFrame";
88 d.style.zIndex = "30";
89 d.style.backgroundColor = "white";
90 // allow resizing modal
91 d.style.overflow = "hidden";
92 d.style.resize = "both";
93 d.style.minWidth = width + "px";
94 d.style.width = width + "px";
95 d.style.height = height + "px";
96 d.shouldDestroy = true;
98 let dlgTitle = document.createElement("div");
99 dlgTitle.className = "dlgTitlebar";
100 dlgTitle.id = "dlgMessageTitle";
101 dlgTitle.innerText = "History " + panel;
102 d.appendChild(dlgTitle);
103 document.body.appendChild(d);
106 // Now we can adjust for the title bar height and dlgPanel padding
107 d.style.height = (height + dlgTitle.offsetHeight + 6) + "px";
108 d.style.minHeight = (height + dlgTitle.offsetHeight + 6) + "px";
110 let dlgPanel = document.createElement("div");
111 dlgPanel.className = "dlgPanel";
112 dlgPanel.style.padding = "3px";
113 dlgPanel.style.minWidth = width + "px";
114 dlgPanel.style.minHeight = height + "px";
115 dlgPanel.style.width = "100%";
116 dlgPanel.style.padding = "0";
117 d.appendChild(dlgPanel);
119 let dlgHistory = document.createElement("div");
120 dlgHistory.className = "mjshistory";
121 if (group !== undefined) {
122 dlgHistory.setAttribute("data-group", group);
123 dlgHistory.setAttribute("data-panel", panel);
125 dlgHistory.setAttribute("data-history-var", panel);
128 dlgHistory.style.height = "100%";
129 dlgPanel.appendChild(dlgHistory);
131 if (x !== undefined && y !== undefined)
134 // initialize history when resizing modal
135 const resizeObs = new ResizeObserver(() => {
136 dlgPanel.style.height = (100 * (d.offsetHeight - dlgTitle.offsetHeight) / d.offsetHeight) + "%";
137 mhistory_init(dlgHistory, true, param);
139 resizeObs.observe(d);
140 // catch event when history dialog is closed
141 const observer = new MutationObserver(([{removedNodes}]) => {
142 if (removedNodes.length && removedNodes[0] === d) {
143 resizeObs.unobserve(d);
144 document.getElementById(dlgHistory.id + "intSel").remove();
145 document.getElementById(dlgHistory.id + "downloadSel").remove();
148 observer.observe(d.parentNode, { childList: true });
153function mhistory_create(parentElement, baseURL, group, panel, tMin, tMax, index) {
154 let d = document.createElement("div");
155 parentElement.appendChild(d);
156 d.dataset.baseURL = baseURL;
157 d.dataset.group = group;
158 d.dataset.panel = panel;
159 d.mhg = new MhistoryGraph(d, undefined, false);
160 if (!Number.isNaN(tMin) && !Number.isNaN(tMax)) {
161 d.mhg.initTMin = tMin;
162 d.mhg.initTMax = tMax;
164 d.mhg.initializePanel(index);
168function mhistory_create_var(parentElement, baseURL, historyVar, tMin, tMax, index) {
169 let d = document.createElement("div");
170 parentElement.appendChild(d);
171 d.dataset.baseURL = baseURL;
172 d.dataset.historyVar = historyVar;
173 d.mhg = new MhistoryGraph(d, undefined, true);
174 if (!Number.isNaN(tMin) && !Number.isNaN(tMax)) {
175 d.mhg.initTMin = tMin;
176 d.mhg.initTMax = tMax;
178 d.mhg.initializePanel(index);
182function getUrlVars() {
184 window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
190function MhistoryGraph(divElement, noBorder, floating) { // Constructor
192 // create canvas inside the div
193 this.parentDiv = divElement;
194 // if absent, generate random string (5 char) to give an id to mhistory
195 if (!this.parentDiv.id)
196 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
197 this.baseURL = divElement.dataset.baseURL;
198 this.group = divElement.dataset.group;
199 this.panel = divElement.dataset.panel;
200 this.historyVar = divElement.dataset.historyVar;
201 this.floating = floating;
202 this.canvas = document.createElement("canvas");
203 if (noBorder !== true)
204 this.canvas.style.border = "1px solid black";
205 this.canvas.style.height = "100%";
206 this.debugString = "";
207 divElement.appendChild(this.canvas);
211 background: "#FFFFFF",
216 "#00AAFF", "#FF9000", "#FF00A0", "#00C030",
217 "#A0C0D0", "#D0A060", "#C04010", "#807060",
218 "#F0C000", "#2090A0", "#D040D0", "#90B000",
219 "#B0B040", "#B0B0FF", "#FFA0A0", "#A0FFA0"],
224 this.yMin0 = undefined;
225 this.yMax0 = undefined;
226 this.tMax = Math.floor(new Date() / 1000);
227 this.tMin = this.tMax - this.tScale;
228 this.yMin = undefined;
229 this.yMax = undefined;
232 this.showZoomButtons = true;
233 this.showMenuButtons = true;
234 this.tMinRequested = 0;
235 this.tMinReceived = 0;
236 this.tMaxRequested = 0;
237 this.tMaxReceived = 0;
238 this.pinchW0 = undefined;
239 this.pinchH0 = undefined;
241 // overwrite scale from URL if present
242 let tMin = decodeURI(getUrlVars()["A"]);
243 if (tMin !== "undefined") {
244 this.initTMin = parseInt(tMin);
245 this.tMin = parseInt(tMin);
247 let tMax = decodeURI(getUrlVars()["B"]);
248 if (tMax !== "undefined") {
249 this.initTMax = parseInt(tMax);
250 this.tMax = parseInt(tMax);
255 this.lastWritten = [];
259 // graph arrays (in screen pixels)
262 // t/v arrays corresponding to x/y
267 // points array with min/max/avg
286 // callbacks when certain actions are performed.
287 // All callback functions should accept a single parameter, which is the
288 // MhistoryGraph object that triggered the callback.
290 resetAxes: undefined,
292 jumpToCurrent: undefined
296 this.marker = {active: false};
297 this.variablesWidth = 0;
298 this.variablesHeight = 0;
301 this.showLabels = false;
304 this.showAxis = true;
307 this.showTitle = true;
310 this.solo = {active: false, index: undefined};
312 // time when panel was drawn last
313 this.lastDrawTime = 0;
314 this.forceRedraw = false;
320 title: "Show / hide legend",
321 click: function (t) {
322 t.showLabels = !t.showLabels;
327 src: "maximize-2.svg",
328 title: "Show only this plot",
329 click: function (t) {
330 window.location.href = t.baseURL + "&group=" + t.group + "&panel=" + t.panel;
334 src: "rotate-ccw.svg",
335 title: "Reset histogram axes",
336 click: function (t) {
339 if (t.callbacks.resetAxes !== undefined) {
340 t.callbacks.resetAxes(t);
346 title: "Jump to current time",
347 click: function (t) {
349 let dt = Math.floor(t.tMax - t.tMin);
351 t.tMax = new Date() / 1000;
352 t.tMin = t.tMax - dt;
355 t.loadFullData(t.tMin, t.tMax, true);
357 if (t.callbacks.jumpToCurrent !== undefined) {
358 t.callbacks.jumpToCurrent(t);
364 title: "Select timespan...",
365 click: function (t) {
366 if (t.intSelector.style.display === "none") {
367 t.intSelector.style.display = "block";
368 t.intSelector.style.left = ((t.canvas.getBoundingClientRect().x + window.pageXOffset +
369 t.x2) - t.intSelector.offsetWidth) + "px";
370 t.intSelector.style.top = (t.canvas.getBoundingClientRect().y + window.pageYOffset +
372 t.intSelector.style.zIndex = "32";
374 t.intSelector.style.display = "none";
380 title: "Download image/data...",
381 click: function (t) {
382 if (t.downloadSelector.style.display === "none") {
383 t.downloadSelector.style.display = "block";
384 t.downloadSelector.style.left = ((t.canvas.getBoundingClientRect().x + window.pageXOffset +
385 t.x2) - t.downloadSelector.offsetWidth) + "px";
386 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.pageYOffset +
388 t.downloadSelector.style.zIndex = "32";
390 t.downloadSelector.style.display = "none";
396 title: "Configure this plot",
397 click: function (t) {
398 window.location.href = "?cmd=hs_edit&group=" + encodeURIComponent(t.group) + "&panel=" + encodeURIComponent(t.panel) + "&redir=" + encodeURIComponent(window.location.href);
402 src: "help-circle.svg",
405 dlgShow("dlgHelp", false);
409 src: "corner-down-left.svg",
410 title: "Return to all variables",
411 click: function (t) {
412 t.solo.active = false;
419 // remove settings for single variable plot
420 if (this.group === undefined) {
421 this.button.splice(1, 1);
422 this.button.splice(5, 1);
426 dlgLoad('dlgHistory.html');
428 this.button.forEach(b => {
430 b.img.src = "icons/" + b.src;
434 this.marker = {active: false};
436 // mouse event handlers
437 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
438 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
439 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
441 divElement.addEventListener("touchstart", this.mouseEvent.bind(this), true);
442 divElement.addEventListener("touchmove", this.mouseEvent.bind(this), true);
443 divElement.addEventListener("touchend", this.mouseEvent.bind(this), true);
445 divElement.addEventListener("wheel", this.mouseWheelEvent.bind(this), true);
446 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
448 // Keyboard event handler (has to be on the window!)
449 window.addEventListener("keydown", this.keyDown.bind(this));
452function timeToSec(str) {
453 let s = parseFloat(str);
454 switch (str[str.length - 1]) {
472function doQueryAB(t) {
474 dlgHide('dlgQueryAB');
477 document.getElementById('y1').value,
478 document.getElementById('m1').selectedIndex,
479 document.getElementById('d1').selectedIndex + 1,
480 document.getElementById('h1').selectedIndex);
483 document.getElementById('y2').value,
484 document.getElementById('m2').selectedIndex,
485 document.getElementById('d2').selectedIndex + 1,
486 document.getElementById('h2').selectedIndex);
491 t.tMin = Math.floor(d1.getTime() / 1000);
492 t.tMax = Math.floor(d2.getTime() / 1000);
495 t.loadFullData(t.tMin, t.tMax);
497 if (t.callbacks.timeZoom !== undefined)
498 t.callbacks.timeZoom(t);
501MhistoryGraph.prototype.keyDown = function (e) {
502 if (e.target && (e.target.isContentEditable || event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')) return;
503 if (e.key === "u") { // 'u' key
504 // force next update t.Min-dt/2 to current time
505 let dt = Math.floor(this.tMax - this.tMin);
507 // limit to one week maximum (otherwise we have to read binned data)
511 this.tMax = new Date() / 1000;
512 this.tMin = this.tMax - dt;
515 this.loadFullData(this.tMin, this.tMax, true);
517 if (this.callbacks.jumpToCurrent !== undefined)
518 this.callbacks.jumpToCurrent(this);
522 if (e.key === "r") { // 'r' key
526 if (e.key === "Escape") {
527 this.solo.active = false;
540MhistoryGraph.prototype.initializePanel = function (index, param) {
542 // initialize variables
543 this.plotIndex = index;
544 this.marker = {active: false};
545 this.drag = {active: false};
546 this.data = undefined;
552 this.pendingUpdates = 0;
554 // single variable mode
555 if (this.parentDiv.dataset.historyVar !== undefined) {
562 "Show run markers": false,
565 "Variables": this.parentDiv.dataset.historyVar.split(','),
567 "Show raw value": false
570 this.param.Label = new Array(this.param.Variables.length).fill('');
571 this.param.Colour = this.color.data.slice(0, this.param.Variables.length);
573 // optionally overwrite default parameters from argument
574 if (param !== undefined)
575 Object.keys(param).forEach(p => {
576 this.param[p] = param[p];
579 this.loadInitialData();
583 // ODB history panel mode
584 this.group = this.parentDiv.dataset.group;
585 this.panel = this.parentDiv.dataset.panel;
587 if (this.group === undefined) {
588 dlgMessage("Error", "Definition of \'dataset-group\' missing for history panel \'" + this.parentDiv.id + "\'. " +
589 "Please use syntax:<br /><br /><b><div class=\"mjshistory\" " +
590 "data-group=\"<Group>\" data-panel=\"<Panel>\"></div></b>", true);
593 if (this.panel === undefined) {
594 dlgMessage("Error", "Definition of \'dataset-panel\' missing for history panel \'" + this.parentDiv.id + "\'. " +
595 "Please use syntax:<br /><br /><b><div class=\"mjshistory\" " +
596 "data-group=\"<Group>\" data-panel=\"<Panel>\"></div></b>", true);
600 if (this.group === "" || this.panel === "")
603 // retrieve panel definition from ODB
604 mjsonrpc_db_copy(["/History/Display/" + this.group + "/" + this.panel]).then(function (rpc) {
605 if (rpc.result.status[0] !== 1) {
606 dlgMessage("Error", "Panel \'" + this.group + "/" + this.panel + "\' not found in ODB", true)
608 let odb = rpc.result.data[0];
610 this.param["Timescale"] = odb["Timescale"];
611 this.param["Minimum"] = odb["Minimum"];
612 this.param["Maximum"] = odb["Maximum"];
613 this.param["Zero ylow"] = odb["Zero ylow"];
614 this.param["Log axis"] = odb["Log axis"];
615 this.param["Show run markers"] = odb["Show run markers"];
616 this.param["Show values"] = odb["Show values"];
617 this.param["Show fill"] = odb["Show fill"];
618 this.param["Variables"] = odb["Variables"];
619 this.param["Formula"] = odb["Formula"];
620 this.param["Colour"] = odb["Colour"];
621 this.param["Label"] = odb["Label"];
622 this.param["Show raw value"] = odb["Show raw value"];
624 this.loadInitialData();
626 }.bind(this)).catch(function (error) {
627 if (error.xhr !== undefined)
628 mjsonrpc_error_alert(error);
635MhistoryGraph.prototype.updateLastWritten = function () {
636 //console.log("update last_written!!!\n");
638 // load date of latest data points
639 mjsonrpc_call("hs_get_last_written",
642 "events": this.events,
645 }).then(function (rpc) {
646 this.lastWritten = rpc.result.last_written;
647 // protect against an infinite loop from draw() if rpc returns invalid times.
648 // by definition, last_written returned by RPC is supposed to be less then tMin.
649 for (let i = 0; i < this.lastWritten.length; i++) {
650 let l = this.lastWritten[i];
651 //console.log("updated last_written: event: " + this.events[i] + ", l: " + l + ", tmin: " + this.tMin + ", diff: " + (l - this.tMin));
653 this.lastWritten[i] = this.tMin;
658 .catch(function (error) {
659 mjsonrpc_error_alert(error);
663MhistoryGraph.prototype.loadInitialData = function () {
665 if (this.initTMin !== undefined && this.initTMin !== "undefined") {
666 this.tMin = this.initTMin;
667 this.tMax = this.initTMax;
668 this.tScale = this.tMax - this.tMin;
671 this.tScale = timeToSec(this.param["Timescale"]);
673 // overwrite via <div ... data-scale=<value> >
674 if (this.parentDiv.dataset.scale !== undefined)
675 this.tScale = timeToSec(this.parentDiv.dataset.scale);
677 this.tMax = Math.floor(new Date() / 1000);
678 this.tMin = this.tMax - this.tScale;
681 this.showLabels = this.param["Show values"];
682 this.showFill = this.param["Show fill"];
684 // overwrite parameters from <div data-xxx> tags
685 if (this.parentDiv.dataset.showValues !== undefined)
686 this.showLabels = this.parentDiv.dataset.showValues === "true" || this.parentDiv.dataset.showValues === "1";
687 if (this.parentDiv.dataset.showFill !== undefined)
688 this.showLabels = this.parentDiv.dataset.showFill === "true" || this.parentDiv.dataset.showFill === "1";
689 if (this.parentDiv.dataset.showAxis !== undefined)
690 this.showAxis = this.parentDiv.dataset.showAxis === "true" || this.parentDiv.dataset.showAxis === "1";
691 if (this.parentDiv.dataset.showTitle !== undefined)
692 this.showTitle = this.parentDiv.dataset.showTitle === "true" || this.parentDiv.dataset.showTitle === "1";
693 if (this.parentDiv.dataset.showMenuButtons !== undefined)
694 this.showMenuButtons = this.parentDiv.dataset.showMenuButtons === "true" || this.parentDiv.dataset.showMenuButtons === "1";
695 if (this.parentDiv.dataset.showZoomButtons !== undefined)
696 this.showZoomButtons = this.parentDiv.dataset.showZoomButtons === "true" || this.parentDiv.dataset.showZoomButtons === "1";
698 this.autoscaleMin = (this.param["Minimum"] === this.param["Maximum"] ||
699 this.param["Minimum"] === "-Infinity" || this.param["Minimum"] === "Infinity");
700 this.autoscaleMax = (this.param["Minimum"] === this.param["Maximum"] ||
701 this.param["Maximum"] === "-Infinity" || this.param["Maximum"] === "Infinity");
703 if (this.param["Zero ylow"]) {
704 this.autoscaleMin = false;
705 this.param["Minimum"] = 0;
708 this.logAxis = this.param["Log axis"];
710 // protect against empty history plot
711 if (!this.param.Variables) {
712 this.param.Variables = "(empty):(empty)";
713 this.param.Label = "(empty)";
714 this.param.Colour = "";
717 // if only one variable present, convert it to array[0]
718 if (!Array.isArray(this.param.Variables))
719 this.param.Variables = new Array(this.param.Variables);
720 if (!Array.isArray(this.param.Label))
721 this.param.Label = new Array(this.param.Label);
722 if (!Array.isArray(this.param.Colour))
723 this.param.Colour = new Array(this.param.Colour);
725 this.param["Variables"].forEach(v => {
726 let event_and_tag = splitEventAndTagName(v);
727 this.events.push(event_and_tag[0]);
728 let t = event_and_tag[1];
729 if (t.indexOf('[') !== -1) {
730 this.tags.push(t.substr(0, t.indexOf('[')));
731 this.index.push(parseInt(t.substr(t.indexOf('[') + 1)));
738 if (this.param["Show run markers"]) {
739 this.events.push("Run transitions");
740 this.events.push("Run transitions");
742 this.tags.push("State");
743 this.tags.push("Run number");
749 let intSelId = this.parentDiv.id + "intSel";
750 if (document.getElementById(intSelId)) document.getElementById(intSelId).remove();
751 this.intSelector = document.createElement("div");
752 this.intSelector.id = intSelId;
753 this.intSelector.style.display = "none";
754 this.intSelector.style.position = "absolute";
755 this.intSelector.className = "mtable";
756 this.intSelector.style.borderRadius = "0";
757 this.intSelector.style.border = "2px solid #808080";
758 this.intSelector.style.margin = "0";
759 this.intSelector.style.padding = "0";
760 this.intSelector.style.left = "100px";
761 this.intSelector.style.top = "100px";
763 let table = document.createElement("table");
767 let buttons = this.param["Buttons"];
768 if (buttons === undefined) {
770 buttons.push("10m", "1h", "3h", "12h", "24h", "3d", "7d");
772 buttons.push("A→B");
773 buttons.push("<<<");
774 buttons.push("<<");
775 buttons.forEach(function (b, i) {
777 row = document.createElement("tr");
779 cell = document.createElement("td");
780 cell.style.padding = "0";
782 link = document.createElement("a");
785 if (b === "A→B")
786 link.title = "Display data between two dates";
787 else if (b === "<<")
788 link.title = "Go back in time to last available data";
789 else if (b === "<<<")
790 link.title = "Go back in time to last available data for all variables on plot";
792 link.title = "Show last " + b;
795 link.onclick = function () {
796 if (b === "A→B") {
797 let currentYear = new Date().getFullYear();
798 let dMin = new Date(this.tMin * 1000);
799 let dMax = new Date(this.tMax * 1000);
801 if (document.getElementById('y1').length === 0) {
802 for (let i = currentYear; i > currentYear - 5; i--) {
803 let o = document.createElement('option');
804 o.value = i.toString();
805 o.appendChild(document.createTextNode(i.toString()));
806 document.getElementById('y1').appendChild(o);
807 o = document.createElement('option');
808 o.value = i.toString();
809 o.appendChild(document.createTextNode(i.toString()));
810 document.getElementById('y2').appendChild(o);
814 document.getElementById('m1').selectedIndex = dMin.getMonth();
815 document.getElementById('d1').selectedIndex = dMin.getDate() - 1;
816 document.getElementById('h1').selectedIndex = dMin.getHours();
817 document.getElementById('y1').selectedIndex = currentYear - dMin.getFullYear();
819 document.getElementById('m2').selectedIndex = dMax.getMonth();
820 document.getElementById('d2').selectedIndex = dMax.getDate() - 1;
821 document.getElementById('h2').selectedIndex = dMax.getHours();
822 document.getElementById('y2').selectedIndex = currentYear - dMax.getFullYear();
824 document.getElementById('dlgQueryQuery').onclick = function () {
828 dlgShow("dlgQueryAB");
830 } else if (b === "<<") {
832 mjsonrpc_call("hs_get_last_written",
835 "events": this.events,
839 .then(function (rpc) {
841 let last = rpc.result.last_written[0];
842 for (let i = 0; i < rpc.result.last_written.length; i++) {
843 if (this.events[i] === "Run transitions") {
846 let l = rpc.result.last_written[i];
847 last = Math.max(last, l);
850 if (last !== 0) { // no data, at all!
851 let scale = mhg.tMax - mhg.tMin;
852 mhg.tMax = last + scale / 2;
853 mhg.tMin = last - scale / 2;
856 mhg.marker.active = false;
858 mhg.loadFullData(mhg.tMin, mhg.tMax);
860 if (mhg.callbacks.timeZoom !== undefined)
861 mhg.callbacks.timeZoom(mhg);
865 .catch(function (error) {
866 mjsonrpc_error_alert(error);
869 } else if (b === "<<<") {
871 mjsonrpc_call("hs_get_last_written",
874 "events": this.events,
878 .then(function (rpc) {
881 for (let i = 0; i < rpc.result.last_written.length; i++) {
882 let l = rpc.result.last_written[i];
883 if (this.events[i] === "Run transitions") {
887 // no data for first variable
889 } else if (l === 0) {
890 // no data for this variable
892 last = Math.min(last, l);
895 //console.log("last: " + last);
897 if (last !== 0) { // no data, at all!
898 let scale = mhg.tMax - mhg.tMin;
899 mhg.tMax = last + scale / 2;
900 mhg.tMin = last - scale / 2;
903 mhg.marker.active = false;
905 mhg.loadFullData(mhg.tMin, mhg.tMax);
907 if (mhg.callbacks.timeZoom !== undefined)
908 mhg.callbacks.timeZoom(mhg);
912 .catch(function (error) {
913 mjsonrpc_error_alert(error);
918 mhg.tMax = new Date() / 1000;
919 mhg.tMin = mhg.tMax - timeToSec(b);
921 mhg.loadFullData(mhg.tMin, mhg.tMax, true);
924 if (mhg.callbacks.timeZoom !== undefined)
925 mhg.callbacks.timeZoom(mhg);
927 mhg.intSelector.style.display = "none";
931 cell.appendChild(link);
932 row.appendChild(cell);
934 table.appendChild(row);
937 if (buttons.length % 2 === 1)
938 table.appendChild(row);
940 this.intSelector.appendChild(table);
941 document.body.appendChild(this.intSelector);
944 let downloadSelId = this.parentDiv.id + "downloadSel";
945 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
946 this.downloadSelector = document.createElement("div");
947 this.downloadSelector.id = downloadSelId;
948 this.downloadSelector.style.display = "none";
949 this.downloadSelector.style.position = "absolute";
950 this.downloadSelector.className = "mtable";
951 this.downloadSelector.style.borderRadius = "0";
952 this.downloadSelector.style.border = "2px solid #808080";
953 this.downloadSelector.style.margin = "0";
954 this.downloadSelector.style.padding = "0";
956 this.downloadSelector.style.left = "100px";
957 this.downloadSelector.style.top = "100px";
959 table = document.createElement("table");
962 row = document.createElement("tr");
963 cell = document.createElement("td");
964 cell.style.padding = "0";
965 link = document.createElement("a");
967 link.innerHTML = "CSV";
968 link.title = "Download data in Comma Separated Value format";
969 link.onclick = function () {
970 mhg.downloadSelector.style.display = "none";
974 cell.appendChild(link);
975 row.appendChild(cell);
976 table.appendChild(row);
978 row = document.createElement("tr");
979 cell = document.createElement("td");
980 cell.style.padding = "0";
981 link = document.createElement("a");
983 link.innerHTML = "PNG";
984 link.title = "Download image in PNG format";
985 link.onclick = function () {
986 mhg.downloadSelector.style.display = "none";
990 cell.appendChild(link);
991 row.appendChild(cell);
992 table.appendChild(row);
994 this.downloadSelector.appendChild(table);
995 document.body.appendChild(this.downloadSelector);
997 // load one window ahead in past and future
998 this.loadFullData(this.tMin - this.tScale/2, this.tMax + this.tScale/2, this.scroll);
1001MhistoryGraph.prototype.loadFullData = function (t1, t2, scrollFlag) {
1003 // retrieve binned data if we request more than one week
1004 this.binned = this.tMax - this.tMin > 3600*24*7;
1006 // don't update in binned mode
1008 this.scroll = false;
1010 // drop current data
1011 this.discardCurrentData();
1013 // prevent future date
1014 let now = Math.floor(new Date() / 1000);
1020 this.tMaxRequested = t2;
1021 this.tMinRequested = t1;
1025 log_hs_read("loadFullData binned", t1, t2);
1026 this.parentDiv.style.cursor = "progress";
1027 this.pendingUpdates++;
1028 mjsonrpc_call("hs_read_binned_arraybuffer",
1033 "events": this.events,
1037 .then(function (rpc) {
1039 this.tMinReceived = this.tMinRequested;
1040 this.tMaxReceived = this.tMaxRequested;
1041 this.pendingUpdates--;
1043 this.receiveDataBinned(rpc);
1047 this.parentDiv.style.cursor = "default";
1050 .catch(function (error) {
1051 mjsonrpc_error_alert(error);
1056 // limit one request to maximum one month
1057 if (t2 - t1 > 3600 * 24 * 30) {
1058 t1 = (t1 + t2)/2 - 3600 * 24 * 15;
1059 t2 = (t1 + t2)/2 + 3600 * 24 * 15;
1060 let now = Math.floor(new Date() / 1000);
1065 log_hs_read("loadFullData un-binned", t1, t2);
1066 this.parentDiv.style.cursor = "progress";
1067 this.pendingUpdates++;
1068 mjsonrpc_call("hs_read_arraybuffer",
1070 "start_time": Math.floor(this.tMinRequested),
1071 "end_time": Math.floor(this.tMaxRequested),
1072 "events": this.events,
1076 .then(function (rpc) {
1078 this.tMinReceived = this.tMinRequested;
1079 this.tMaxReceived = this.tMaxRequested;
1080 this.pendingUpdates--;
1082 this.receiveData(rpc);
1087 this.loadNewData(); // triggers scrolling
1089 this.parentDiv.style.cursor = "default";
1092 .catch(function (error) {
1093 mjsonrpc_error_alert(error);
1098MhistoryGraph.prototype.loadSideData = function () {
1100 let dt = this.tMaxReceived - this.tMinReceived;
1103 // check for left side data
1104 if (this.tMin < this.tMinRequested) {
1106 t1 = this.tMin - dt; // request one window
1107 t2 = this.tMinReceived;
1108 this.tMinRequested = t1;
1111 log_hs_read("loadSideData left binned", t1, t2);
1112 this.parentDiv.style.cursor = "progress";
1113 this.pendingUpdates++;
1114 mjsonrpc_call("hs_read_binned_arraybuffer",
1119 "events": this.events,
1123 .then(function (rpc) {
1125 this.tMinReceived = this.tMinRequested;
1126 this.pendingUpdates--;
1128 this.receiveDataBinned(rpc);
1132 this.parentDiv.style.cursor = "default";
1135 .catch(function (error) {
1136 mjsonrpc_error_alert(error);
1139 } else { // un-binned
1141 log_hs_read("loadSideData left un-binned", t1, t2);
1142 this.pendingUpdates++;
1143 mjsonrpc_call("hs_read_arraybuffer",
1147 "events": this.events,
1151 .then(function (rpc) {
1153 this.tMinReceived = this.tMinRequested;
1154 this.pendingUpdates--;
1156 this.receiveData(rpc);
1160 this.parentDiv.style.cursor = "default";
1163 .catch(function (error) {
1164 mjsonrpc_error_alert(error);
1169 // check for right side data
1170 if (this.tMax > this.tMaxRequested) {
1172 t1 = this.tMaxReceived;
1173 t2 = this.tMax + dt; // request one window
1174 this.tMaxRequested = t2;
1177 log_hs_read("loadSideData right binned", t1, t2);
1178 this.parentDiv.style.cursor = "progress";
1179 this.pendingUpdates++;
1180 mjsonrpc_call("hs_read_binned_arraybuffer",
1185 "events": this.events,
1189 .then(function (rpc) {
1191 this.tMaxReceived = this.tMaxRequested;
1192 this.pendingUpdates--;
1194 this.receiveDataBinned(rpc);
1198 this.parentDiv.style.cursor = "default";
1201 .catch(function (error) {
1202 mjsonrpc_error_alert(error);
1205 } else { // un-binned
1207 this.parentDiv.style.cursor = "progress";
1208 log_hs_read("loadSideData right un-binned", t1, t2);
1209 this.pendingUpdates++;
1210 mjsonrpc_call("hs_read_arraybuffer",
1214 "events": this.events,
1218 .then(function (rpc) {
1220 this.tMaxReceived = this.tMaxRequested;
1221 this.pendingUpdates--;
1223 this.receiveData(rpc);
1227 this.parentDiv.style.cursor = "default";
1230 .catch(function (error) {
1231 mjsonrpc_error_alert(error);
1237MhistoryGraph.prototype.receiveData = function (rpc) {
1239 // decode binary array
1240 let array = new Float64Array(rpc);
1241 let nVars = array[1];
1242 let nData = array.slice(2 + nVars, 2 + 2 * nVars);
1243 let i = 2 + 2 * nVars;
1245 if (i >= array.length) {
1246 // RPC did not return any data
1248 if (this.data === undefined) {
1249 // must initialize the arrays otherwise nothing works
1251 for (let index = 0; index < nVars; index++) {
1252 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1259 // bin size 1 for un-binned data
1262 // push empty arrays on the first time
1263 if (this.data === undefined) {
1265 for (let index = 0; index < nVars; index++) {
1266 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1270 // append new values to end of arrays
1271 for (let index = 0; index < nVars; index++) {
1272 if (nData[index] === 0)
1275 let formula = this.param["Formula"];
1276 if (Array.isArray(formula))
1277 formula = formula[index];
1283 if (formula !== undefined && formula !== "") {
1284 for (let j = 0; j < nData[index]; j++) {
1293 for (let j = 0; j < nData[index]; j++) {
1301 if (t1.length > 0) {
1303 let da = new Date(t1[0]*1000);
1304 let db = new Date(t1[t1.length-1]*1000);
1307 log_hs_read("receiveData un-binned", t1[0], t1[t1.length-1]);
1309 let told = this.data[index].time;
1311 if (this.data[index].time.length === 0 ||
1312 t1[t1.length-1] >= this.data[index].time[this.data[index].time.length-1]) {
1314 // remove overlapping data
1315 if (this.data[index].time.length > 0)
1316 while (t1[0] <= this.data[index].time[this.data[index].time.length-1]) {
1319 if (v1Raw.length > 0)
1320 v1Raw = v1Raw.slice(1);
1323 // add data to the right
1324 this.data[index].time = this.data[index].time.concat(t1);
1325 this.data[index].value = this.data[index].value.concat(v1);
1326 if (v1Raw.length > 0)
1327 this.data[index].rawValue = this.data[index].rawValue.concat(v1Raw);
1331 // strip overlapping data
1332 while (t1[t1.length-1] >= this.data[index].time[0]) {
1335 if (v1Raw.length > 0)
1339 // add data to the left
1340 this.data[index].time = t1.concat(this.data[index].time);
1341 this.data[index].value = v1.concat(this.data[index].value);
1342 if (v1Raw.length > 0)
1343 this.data[index].rawValue = v1Raw.concat(this.data[index].rawValue);
1347 for (let i = 1; i < this.data[index].time.length; i++)
1348 if (this.data[index].time[i] < this.data[index].time[i - 1]) {
1349 console.log("Error non-continuous data");
1350 log_hs_read("told", told[0], told[told.length-1]);
1351 log_hs_read("t1", t1[0], t1[t1.length-1]);
1360MhistoryGraph.prototype.receiveDataBinned = function (rpc) {
1362 // decode binary array
1363 let array = new Float64Array(rpc);
1365 // let status = array[0];
1366 // let startTime = array[1];
1367 // let endTime = array[2];
1368 let numBins = array[3];
1369 let nVars = array[4];
1372 // let hsStatus = array.slice(i, i + nVars);
1374 let numEntries = array.slice(i, i + nVars);
1376 // let lastTime = array.slice(i, i + nVars);
1378 // let lastValue = array.slice(i, i + nVars);
1381 if (i >= array.length) {
1382 // RPC did not return any data
1384 if (this.data === undefined) {
1385 // must initialize the arrays otherwise nothing works
1387 for (let index = 0; index < nVars; index++) {
1388 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1395 // push empty arrays on the first time
1396 if (this.data === undefined) {
1398 for (let index = 0; index < nVars; index++) {
1399 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1406 // create arrays of new values
1407 for (let index = 0; index < nVars; index++) {
1408 if (numEntries[index] === 0)
1415 // add data to the right
1416 let formula = this.param["Formula"];
1417 if (Array.isArray(formula))
1418 formula = formula[index];
1420 if (formula === undefined || formula === "") {
1421 for (let j = 0; j < numBins; j++) {
1423 let count = array[i++];
1424 // let mean = array[i++];
1425 // let rms = array[i++];
1427 let minValue = array[i++];
1428 let maxValue = array[i++];
1429 let firstTime = array[i++];
1430 let firstValue = array[i++];
1431 let lastTime = array[i++];
1432 let lastValue = array[i++];
1433 let t = Math.floor((firstTime + lastTime) / 2);
1436 // append to the right
1441 bin.firstValue = firstValue;
1442 bin.lastValue = lastValue;
1443 bin.minValue = minValue;
1444 bin.maxValue = maxValue;
1448 // calculate average bin count
1454 } else { // use formula
1456 for (let j = 0; j < numBins; j++) {
1458 let count = array[i++];
1459 // let mean = array[i++];
1460 // let rms = array[i++];
1462 let minValue = array[i++];
1463 let maxValue = array[i++];
1464 let firstTime = array[i++];
1465 let firstValue = array[i++];
1466 let lastTime = array[i++];
1467 let lastValue = array[i++];
1470 // append to the right
1471 t1.push(Math.floor((firstTime + lastTime) / 2));
1476 binRaw.firstValue = firstValue;
1477 bin.firstValue = eval(formula);
1479 binRaw.lastValue = lastValue;
1480 bin.lastValue = eval(formula);
1482 binRaw.minValue = minValue;
1483 bin.minValue = eval(formula);
1485 binRaw.maxValue = maxValue;
1486 bin.maxValue = eval(formula);
1489 binRaw1.push(binRaw);
1491 // calculate average bin count
1498 if (t1.length > 0) {
1500 let da = new Date(t1[0]*1000);
1501 let db = new Date(t1[t1.length-1]*1000);
1504 log_hs_read("receiveData binned", t1[0], t1[t1.length-1]);
1506 if (this.data[index].time.length === 0 ||
1507 t1[0] > this.data[index].time[0]) {
1509 // append to right if new data
1510 this.data[index].time = this.data[index].time.concat(t1);
1511 this.data[index].bin = this.data[index].bin.concat(bin1);
1512 if (binRaw1.length > 0)
1513 this.data[index].binRaw = this.data[index].rawValue.concat(binRaw1);
1517 // append to left if old data
1518 this.data[index].time = t1.concat(this.data[index].time);
1519 this.data[index].bin = bin1.concat(this.data[index].bin);
1520 if (binRaw1.length > 0)
1521 this.data[index].binRaw = binRaw1.concat(this.data[index].binRaw);
1526 // calculate average bin size
1528 this.binSize = binSize / binSizeN;
1533MhistoryGraph.prototype.loadNewData = function () {
1535 if (this.updateTimer)
1536 window.clearTimeout(this.updateTimer);
1538 // don't update window if content is hidden (other tab, minimized, etc.)
1539 if (document.hidden) {
1540 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1544 // don't update if not in scrolling mode
1546 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1550 // update data from last point to current time
1551 let t1 = this.tMaxReceived;
1552 if (t1 === undefined)
1554 t1 -= 1; // add one second overlap in case any history data arrived late
1555 let t2 = Math.floor(new Date() / 1000);
1557 // for strip-chart mode always use non-binned data
1558 this.binned = false;
1560 log_hs_read("loadNewData un-binned", t1, t2);
1561 mjsonrpc_call("hs_read_arraybuffer",
1563 "start_time": Math.floor(t1),
1564 "end_time": Math.floor(t2),
1565 "events": this.events,
1569 .then(function (rpc) {
1571 if (this.tMinRequested === undefined || t1 < this.tMinRequested) {
1572 this.tMinRequested = t1;
1573 this.tMinReceived = t1;
1576 if (this.tMaxRequested === undefined || t2 > this.tMaxRequested) {
1577 this.tMaxReceived = t2;
1578 this.tMaxRequested = t2;
1581 if (this.receiveData(rpc)) {
1583 this.scrollRedraw();
1586 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 1000);
1588 }.bind(this)).catch(function (error) {
1589 mjsonrpc_error_alert(error);
1593MhistoryGraph.prototype.scrollRedraw = function () {
1594 if (this.scrollTimer)
1595 window.clearTimeout(this.scrollTimer);
1598 let dt = this.tMax - this.tMin;
1599 this.tMax = new Date() / 1000;
1600 this.tMin = this.tMax - dt;
1604 // calculate time for one pixel
1605 dt = (this.tMax - this.tMin) / (this.x2 - this.x1);
1606 dt = Math.min(Math.max(0.1, dt), 60);
1607 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), dt / 2 * 1000);
1609 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
1614MhistoryGraph.prototype.discardCurrentData = function () {
1616 if (this.data === undefined)
1620 for (let i = 0 ; i<this.data.length ; i++) {
1621 this.data[i].time.length = 0;
1622 if (this.data[i].value)
1623 this.data[i].value.length = 0;
1624 if (this.data[i].bin)
1625 this.data[i].bin.length = 0;
1626 if (this.data[i].rawBin)
1627 this.data[i].rawBin.length = 0;
1628 if (this.data[i].rawValue)
1629 this.data[i].rawValue.length = 0;
1631 this.x[i].length = 0;
1632 this.y[i].length = 0;
1633 this.t[i].length = 0;
1634 this.v[i].length = 0;
1636 this.vRaw[i].length = 0;
1639 this.tMaxReceived = undefined ;
1640 this.tMaxRequested = undefined;
1641 this.tMinRequested = undefined;
1642 this.tMinReceived = undefined;
1645function binarySearch(array, target) {
1647 let endIndex = array.length - 1;
1649 while (startIndex <= endIndex) {
1650 middleIndex = Math.floor((startIndex + endIndex) / 2);
1651 if (target === array[middleIndex])
1654 if (target > array[middleIndex])
1655 startIndex = middleIndex + 1;
1656 if (target < array[middleIndex])
1657 endIndex = middleIndex - 1;
1663function splitEventAndTagName(var_name) {
1666 for (let i = 0; i < var_name.length; i++) {
1667 if (var_name[i] == ':') {
1672 let slash_pos = var_name.indexOf("/");
1673 let uses_per_variable_naming = (slash_pos != -1);
1675 if (uses_per_variable_naming && colons.length % 2 == 1) {
1676 let middle_colon_pos = colons[Math.floor(colons.length / 2)];
1677 let slash_to_mid = var_name.substr(slash_pos + 1, middle_colon_pos - slash_pos - 1);
1678 let mid_to_end = var_name.substr(middle_colon_pos + 1);
1680 if (slash_to_mid == mid_to_end) {
1681 // Special case - we have a string of the form Beamlime/GS2:FC1:GS2:FC1.
1682 // Logger has already warned people that having colons in the equipment/event
1683 // names is a bad idea, so we only need to worry about them in the tag name.
1684 split_pos = middle_colon_pos;
1686 // We have a string of the form Beamlime/Demand:GS2:FC1. Split at the first colon.
1687 split_pos = colons[0];
1690 // Normal case - split at the fist colon.
1691 split_pos = colons[0];
1694 let event_name = var_name.substr(0, split_pos);
1695 let tag_name = var_name.substr(split_pos + 1);
1697 return [event_name, tag_name];
1700MhistoryGraph.prototype.mouseEvent = function (e) {
1702 // fix buttons for IE
1703 if (!e.which && e.button) {
1704 if ((e.button & 1) > 0) e.which = 1; // Left
1705 else if ((e.button & 4) > 0) e.which = 2; // Middle
1706 else if ((e.button & 2) > 0) e.which = 3; // Right
1709 // extract X and Y coordinates
1712 if (e.type === "touchstart" || e.type === "touchmove") {
1713 eventX = e.touches[0].clientX;
1714 eventY = e.touches[0].clientY;
1715 let rect = e.target.getBoundingClientRect();
1716 eventX = eventX - Math.round(rect.left);
1717 eventY = eventY - Math.round(rect.top);
1718 } else if (e.type === "mousedown" || e.type === "mousemove" || e.type === "mouseup" ||
1719 e.type === "dblclick") {
1724 let cursor = this.pendingUpdates > 0 ? "progress" : "default";
1728 // cancel dragging in case we did not catch the mouseup event
1729 if (e.type === "mousemove" && e.buttons === 0 &&
1730 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
1733 if (e.type === "mousedown" || (e.type === "touchstart" && e.touches.length === 1)) {
1735 this.intSelector.style.display = "none";
1736 this.downloadSelector.style.display = "none";
1738 // check for buttons
1739 this.button.forEach(b => {
1740 if (eventX > b.x1 && eventX < b.x1 + b.width &&
1741 eventY > b.y1 && eventY < b.y1 + b.width &&
1747 // check for zoom buttons
1749 if (this.showMenuButtons)
1750 xb = this.width - 26 - 40;
1752 xb = this.width - 41;
1753 if (eventX > xb && eventX < xb + 20 &&
1754 eventY > this.y1 - 20 && eventY < this.y1) {
1756 let delta = this.tMax - this.tMin;
1758 this.tMin += delta / 2; // only zoom on left side in scroll mode
1760 this.tMin += delta / 4;
1761 this.tMax -= delta / 4; // zoom to center
1764 this.loadFullData(this.tMin, this.tMax);
1766 if (this.callbacks.timeZoom !== undefined)
1767 this.callbacks.timeZoom(this);
1772 if (eventX > xb + 20 && eventX < xb + 40 &&
1773 eventY > this.y1 - 20 && eventY < this.y1) {
1775 if (this.pendingUpdates > 0) {
1776 dlgMessage("Warning", "Don't press the '-' too fast!", true, false);
1778 let delta = this.tMax - this.tMin;
1779 this.tMin -= delta / 2;
1780 this.tMax += delta / 2;
1781 // don't go into the future
1782 let now = Math.floor(new Date() / 1000);
1783 if (this.tMax > now) {
1785 this.tMin = now - 2*delta;
1788 this.loadFullData(this.tMin, this.tMax);
1790 if (this.callbacks.timeZoom !== undefined)
1791 this.callbacks.timeZoom(this);
1794 if (e.type === "mousedown" ) {
1800 // check for dragging
1801 if (eventX > this.x1 && eventX < this.x2 &&
1802 eventY > this.y2 && eventY < this.y1) {
1803 this.drag.active = true;
1804 this.marker.active = false;
1805 this.drag.xStart = eventX;
1806 this.drag.yStart = eventY;
1807 this.drag.tStart = this.xToTime(eventX);
1808 this.drag.tMinStart = this.tMin;
1809 this.drag.tMaxStart = this.tMax;
1810 this.drag.yMinStart = this.yMin;
1811 this.drag.yMaxStart = this.yMax;
1812 this.drag.vStart = this.yToValue(eventY);
1815 // check for axis dragging
1816 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1) {
1817 this.zoom.x.active = true;
1818 this.scroll = false;
1819 this.zoom.x.x1 = eventX;
1820 this.zoom.x.x2 = undefined;
1821 this.zoom.x.t1 = this.xToTime(eventX);
1823 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1) {
1824 this.zoom.y.active = true;
1825 this.scroll = false;
1826 this.zoom.y.y1 = eventY;
1827 this.zoom.y.y2 = undefined;
1828 this.zoom.y.v1 = this.yToValue(eventY);
1833 if (cancel || e.type === "mouseup" || e.type === "touchend") {
1835 if (this.drag.active) {
1836 this.drag.active = false;
1839 if (this.zoom.x.active) {
1840 if (this.zoom.x.x2 !== undefined &&
1841 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
1842 let t1 = this.zoom.x.t1;
1843 let t2 = this.xToTime(this.zoom.x.x2);
1845 [t1, t2] = [t2, t1];
1851 this.zoom.x.active = false;
1853 this.loadFullData(this.tMin, this.tMax);
1855 if (this.callbacks.timeZoom !== undefined)
1856 this.callbacks.timeZoom(this);
1859 if (this.zoom.y.active) {
1860 if (this.zoom.y.y2 !== undefined &&
1861 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
1862 let v1 = this.zoom.y.v1;
1863 let v2 = this.yToValue(this.zoom.y.y2);
1865 [v1, v2] = [v2, v1];
1869 this.zoom.y.active = false;
1877 if (e.type === "touchstart" && e.touches.length === 2) {
1880 this.scroll = false;
1882 // start pinch / zoom
1884 let rect = e.target.getBoundingClientRect();
1886 this.zoom.x.pinch = true;
1887 this.zoom.x.x1 = e.touches[0].clientX - Math.round(rect.left);
1888 this.zoom.x.x2 = e.touches[1].clientX - Math.round(rect.left);
1889 this.zoom.x.t1 = this.xToTime(this.zoom.x.x1);
1890 this.zoom.x.t2 = this.xToTime(this.zoom.x.x2);
1892 this.zoom.y.pinch = true;
1893 this.zoom.y.y1 = e.touches[0].clientY - Math.round(rect.top);
1894 this.zoom.y.y2 = e.touches[1].clientY - Math.round(rect.top);
1895 this.zoom.y.v1 = this.yToValue(this.zoom.y.y1);
1896 this.zoom.y.v2 = this.yToValue(this.zoom.y.y2);
1898 let w = Math.abs(this.zoom.x.x2 - this.zoom.x.x1);
1899 let h = Math.abs(this.zoom.y.y2 - this.zoom.y.y1);
1902 this.zoom.x.pinch = false;
1904 this.zoom.y.pinch = false;
1906 if (this.zoom.y.pinch)
1911 if (e.type === "touchmove" && e.touches.length === 2) {
1915 let rect = e.target.getBoundingClientRect();
1916 let x1 = e.touches[0].clientX - Math.round(rect.left);
1917 let x2 = e.touches[1].clientX - Math.round(rect.left);
1918 let y1 = e.touches[0].clientY - Math.round(rect.top);
1919 let y2 = e.touches[1].clientY - Math.round(rect.top);
1921 // solution to linear equation:
1922 // xToTime(x1) =!= this.zoom.x.t1
1923 // xToTime(x2) =!= this.zoom.x.t2
1925 if (this.zoom.x.pinch) {
1926 let a = (x1 - this.x1) / (this.x2 - this.x1);
1927 let b = (x2 - this.x1) / (this.x2 - this.x1);
1929 let tMin = (a * this.zoom.x.t2 - b * this.zoom.x.t1) / (a - b);
1930 let tMax = ((a - 1) * this.zoom.x.t2 - (b - 1) * this.zoom.x.t1) / (a - b);
1932 if (tMax > tMin + 10) {
1937 this.loadSideData();
1940 if (this.zoom.y.pinch) {
1941 let a = (this.y1 - y1) / (this.y1 - this.y2);
1942 let b = (this.y1 - y2) / (this.y1 - this.y2);
1944 let yMin = (a * this.zoom.y.v2 - b * this.zoom.y.v1) / (a - b);
1945 let yMax = ((a - 1) * this.zoom.y.v2 - (b - 1) * this.zoom.y.v1) / (a - b);
1956 if (e.type === "touchend") {
1957 if (this.zoom.x.pinch)
1958 this.loadFullData(this.tMin, this.tMax);
1960 this.zoom.x.pinch = false;
1961 this.zoom.y.pinch = false;
1964 if (e.type === "mousemove" || ((e.type === "touchmove" || e.type === "touchstart") && e.touches.length === 1) ) {
1966 if (this.drag.active) {
1969 this.scroll = false;
1973 let dt = Math.round((eventX - this.drag.xStart) / (this.x2 - this.x1) * (this.tMax - this.tMin));
1974 this.tMin = this.drag.tMinStart - dt;
1975 this.tMax = this.drag.tMaxStart - dt;
1976 this.drag.lastDt = (eventX - this.drag.lastOffsetX) / (this.x2 - this.x1) * (this.tMax - this.tMin);
1977 this.drag.lastT = new Date().getTime();
1978 this.drag.lastOffsetX = eventX;
1984 let dy = eventY - this.drag.yStart;
1986 this.yMin = Math.exp((this.y1 - (this.y1 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
1987 this.yMax = Math.exp((this.y1 - (this.y2 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
1995 let dy = (this.drag.yStart - eventY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
1996 this.yMin = this.drag.yMinStart - dy;
1997 this.yMax = this.drag.yMaxStart - dy;
2002 this.loadSideData();
2006 if (this.callbacks.timeZoom !== undefined)
2007 this.callbacks.timeZoom(this);
2011 if (!this.drag.active || e.type === "touchstart" || e.type === "touchmove") {
2015 // change cursor to pointer over buttons
2016 this.button.forEach(b => {
2017 if (eventX > b.x1 && eventY > b.y1 &&
2018 eventX < b.x1 + b.width && eventY < b.y1 + b.height) {
2024 if (this.showZoomButtons) {
2027 if (this.showMenuButtons)
2028 xb = this.width - 26 - 40;
2030 xb = this.width - 41;
2032 // check for zoom buttons
2033 if (eventX > xb && eventX < xb + 20 &&
2034 eventY > this.y1 - 20 && eventY < this.y1) {
2038 if (eventX > xb + 20 && eventX < xb + 40 &&
2039 eventY > this.y1 - 20 && eventY < this.y1) {
2045 // display zoom cursor
2046 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1)
2047 cursor = "ew-resize";
2048 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1)
2049 cursor = "ns-resize";
2051 // execute axis zoom
2052 if (this.zoom.x.active) {
2053 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, eventX));
2054 this.zoom.x.t2 = this.xToTime(eventX);
2057 if (this.zoom.y.active) {
2058 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, eventY));
2059 this.zoom.y.v2 = this.yToValue(eventY);
2063 // check if cursor close to graph point
2064 if (this.data !== undefined && this.x.length && this.y.length) {
2066 let minDist = 10000;
2067 let markerX, markerY, markerT, markerV;
2068 for (let di = 0; di < this.data.length; di++) {
2070 if (this.solo.active && di !== this.solo.index)
2073 let i1 = binarySearch(this.x[di], eventX - 10);
2074 let i2 = binarySearch(this.x[di], eventX + 10);
2077 for (let i = i1; i <= i2; i++) {
2078 let d = (eventX - this.x[di][i]) * (eventX - this.x[di][i]) +
2079 (eventY - this.y[di][i]) * (eventY - this.y[di][i]);
2082 markerX = this.x[di][i];
2083 markerY = this.y[di][i];
2084 markerT = this.t[di][i];
2085 markerV = this.v[di][i];
2087 if (this.param["Show raw value"] !== undefined &&
2088 this.param["Show raw value"][di])
2089 markerV = this.vRaw[di][i];
2091 markerV = this.v[di][i];
2093 this.marker.graphIndex = di;
2094 this.marker.index = i;
2100 for (let i = i1; i <= i2; i++) {
2101 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2102 (eventY - this.p[di][i].max) * (eventY - this.p[di][i].max);
2105 markerX = this.p[di][i].x;
2106 markerY = this.p[di][i].max;
2107 markerT = this.p[di][i].t;
2109 if (this.param["Show raw value"] !== undefined &&
2110 this.param["Show raw value"][di])
2111 markerV = this.p[di][i].rawMaxValue;
2113 markerV = this.p[di][i].maxValue;
2115 this.marker.graphIndex = di;
2116 this.marker.index = i;
2121 for (let i = i1; i <= i2; i++) {
2122 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2123 (eventY - this.p[di][i].min) * (eventY - this.p[di][i].min);
2126 markerX = this.p[di][i].x;
2127 markerY = this.p[di][i].min;
2128 markerT = this.p[di][i].t;
2130 if (this.param["Show raw value"] !== undefined &&
2131 this.param["Show raw value"][di])
2132 markerV = this.p[di][i].rawMinValue;
2134 markerV = this.p[di][i].minValue;
2136 this.marker.graphIndex = di;
2137 this.marker.index = i;
2144 // exclude zoom buttons if visible
2145 let exclude = false;
2146 if (this.showZoomButtons &&
2147 eventX > this.width - 26 - 40 && this.offsetX < this.width - 26 &&
2148 eventY > this.y1 - 20 && eventY < this.y1) {
2151 // exclude label area
2152 if (this.showLabels &&
2153 eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7 &&
2154 eventY > this.y2 && eventY < this.y2 + this.variablesHeight + 2) {
2159 this.marker.active = false;
2161 this.marker.active = Math.sqrt(minDist) < 10 && eventX > this.x1 && eventX < this.x2;
2162 if (this.marker.active) {
2163 this.marker.x = markerX;
2164 this.marker.y = markerY;
2165 this.marker.t = markerT;
2166 this.marker.v = markerV;
2168 this.marker.mx = eventX;
2169 this.marker.my = eventY;
2172 if (this.marker.active)
2174 if (!this.marker.active && this.marker.activeOld)
2176 this.marker.activeOld = this.marker.active;
2185 if (e.type === "dblclick") {
2187 // check if inside zoom buttons
2188 if (eventX > this.width - 26 - 40 && eventX < this.width - 26 &&
2189 eventY > this.y1 - 20 && eventY < this.y1) {
2194 // measure distance to graphs
2195 if (this.data !== undefined && this.x.length && this.y.length) {
2197 // check if inside label area
2199 if (this.showLabels) {
2200 if (eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7) {
2201 let i = Math.floor((eventY - (this.y2 + 4)) / 17);
2202 if (i < this.data.length) {
2203 this.solo.active = true;
2204 this.solo.index = i;
2213 for (let di = 0; di < this.data.length; di++) {
2214 for (let i = 0; i < this.x[di].length; i++) {
2215 if (this.x[di][i] > this.x1 && this.x[di][i] < this.x2) {
2216 let d = Math.sqrt(Math.pow(eventX - this.x[di][i], 2) +
2217 Math.pow(eventY - this.y[di][i], 2));
2220 this.solo.index = di;
2225 // check if close to graph point
2226 if (minDist < 10 && eventX > this.x1 && eventX < this.x2)
2227 this.solo.active = !this.solo.active;
2236 this.parentDiv.title = title;
2237 this.parentDiv.style.cursor = cursor;
2242MhistoryGraph.prototype.mouseWheelEvent = function (e) {
2244 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2245 e.offsetY > this.y2 && e.offsetY < this.y1) {
2247 if (e.altKey || e.shiftKey) {
2251 let f = (e.offsetY - this.y1) / (this.y2 - this.y1);
2253 let step = e.deltaY / 100;
2259 let dtMin = f * (this.yMax - this.yMin) * step;
2260 let dtMax = (1 - f) * (this.yMax - this.yMin) * step;
2262 if (((this.yMax + dtMax) - (this.yMin - dtMin)) / (this.yMax0 - this.yMin0) < 1000 &&
2263 (this.yMax0 - this.yMin0) / ((this.yMax + dtMax) - (this.yMin - dtMin)) < 1000) {
2267 if (this.logAxis && this.yMin <= 0)
2269 if (this.logAxis && this.yMax <= 0)
2275 } else if (e.ctrlKey || e.metaKey) {
2277 this.showZoomButtons = false;
2280 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2281 let m = e.deltaY / 100;
2286 let dtMin = Math.abs(f * (this.tMax - this.tMin) * m);
2287 let dtMax = Math.abs((1 - f) * (this.tMax - this.tMin) * m);
2298 this.loadFullData(this.tMin, this.tMax);
2309 this.loadFullData(this.tMin, this.tMax);
2312 if (this.callbacks.timeZoom !== undefined)
2313 this.callbacks.timeZoom(this);
2315 } else if (e.deltaX !== 0) {
2317 let dt = (this.tMax - this.tMin) / 1000 * e.deltaX;
2322 this.loadSideData();
2328 this.marker.active = false;
2334MhistoryGraph.prototype.resetAxes = function () {
2335 this.tMax = Math.floor(new Date() / 1000);
2336 this.tMin = this.tMax - this.tScale;
2340 this.showZoomButtons = true;
2341 this.loadFullData(this.tMin, this.tMax);
2344MhistoryGraph.prototype.jumpToCurrent = function () {
2345 let dt = Math.floor(this.tMax - this.tMin);
2347 // limit to one week maximum (otherwise we have to read binned data)
2351 this.tMax = Math.floor(new Date() / 1000);
2352 this.tMin = this.tMax - dt;
2355 this.loadFullData(this.tMin, this.tMax);
2358MhistoryGraph.prototype.setTimespan = function (tMin, tMax, scroll) {
2361 this.scroll = scroll;
2363 this.loadFullData(tMin, tMax, scroll);
2366MhistoryGraph.prototype.resize = function () {
2367 this.canvas.width = this.parentDiv.clientWidth;
2368 this.canvas.height = this.parentDiv.clientHeight;
2369 this.width = this.parentDiv.clientWidth;
2370 this.height = this.parentDiv.clientHeight;
2372 if (this.intSelector !== undefined)
2373 this.intSelector.style.display = "none";
2375 this.forceConvert = true;
2379MhistoryGraph.prototype.redraw = function (force) {
2380 this.forceRedraw = force;
2381 let f = this.draw.bind(this);
2382 window.requestAnimationFrame(f);
2385MhistoryGraph.prototype.timeToXInit = function () {
2386 this.timeToXScale = 1 / (this.tMax - this.tMin) * (this.x2 - this.x1);
2389MhistoryGraph.prototype.timeToX = function (t) {
2390 return (t - this.tMin) * this.timeToXScale + this.x1;
2393MhistoryGraph.prototype.truncateInfinity = function(v) {
2394 if (v === Infinity) {
2395 return Number.MAX_VALUE;
2396 } else if (v === -Infinity) {
2397 return -Number.MAX_VALUE;
2403MhistoryGraph.prototype.valueToYInit = function () {
2404 // Avoid overflow of max - min > inf
2405 let max_scaled = this.yMax / 1e4;
2406 let min_scaled = this.yMin / 1e4;
2407 this.valueToYScale = (this.y1 - this.y2) * 1e-4 / (max_scaled - min_scaled);
2410MhistoryGraph.prototype.valueToY = function (v) {
2411 if (v === Infinity) {
2412 return this.yMax >= Number.MAX_VALUE ? this.y2 : 0;
2413 } else if (v === -Infinity) {
2414 return this.yMin <= -Number.MAX_VALUE ? this.y1 : this.y1 * 2;
2415 } else if (this.logAxis) {
2419 return this.y1 - (Math.log(v) - Math.log(this.yMin)) /
2420 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
2422 return this.y1 - (v - this.yMin) * this.valueToYScale;
2426MhistoryGraph.prototype.xToTime = function (x) {
2427 return (x - this.x1) / (this.x2 - this.x1) * (this.tMax - this.tMin) + this.tMin;
2430MhistoryGraph.prototype.yToValue = function (y) {
2431 if (!isFinite(this.yMax - this.yMin)) {
2432 // Contortions to avoid Infinity.
2433 let scaled = (this.yMax / 1e4) - (this.yMin / 1e4);
2434 let retval = ((((this.y1 - y) / (this.y1 - this.y2)) * scaled) + (this.yMin / 1e4)) * 1e4;
2438 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
2439 return Math.exp(yl);
2441 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
2444MhistoryGraph.prototype.findMinMax = function () {
2449 if (!this.autoscaleMin)
2450 this.yMin0 = this.param["Minimum"];
2452 if (!this.autoscaleMax)
2453 this.yMax0 = this.param["Maximum"];
2455 if (!this.autoscaleMin && !this.autoscaleMax) {
2456 this.yMin = this.yMin0;
2457 this.yMax = this.yMax0;
2461 let minValue = undefined;
2462 let maxValue = undefined;
2463 for (let index = 0; index < this.data.length; index++) {
2464 if (this.events[index] === "Run transitions")
2466 if (this.data[index].time.length === 0)
2468 if (this.solo.active && this.solo.index !== index)
2470 let i1 = binarySearch(this.data[index].time, this.tMin) + 1;
2471 let i2 = binarySearch(this.data[index].time, this.tMax);
2472 while ((minValue === undefined ||
2473 maxValue === undefined ||
2474 Number.isNaN(minValue) ||
2475 Number.isNaN(maxValue)) &&
2477 // find first valid value
2479 if (this.data[index].bin[i1].count !== 0) {
2480 minValue = this.data[index].bin[i1].minValue;
2481 maxValue = this.data[index].bin[i1].maxValue;
2484 minValue = this.data[index].value[i1];
2485 maxValue = this.data[index].value[i1];
2489 for (let i = i1; i <= i2; i++) {
2491 if (this.data[index].bin[i].count === 0)
2493 let v = this.data[index].bin[i].minValue;
2496 v = this.data[index].bin[i].maxValue;
2500 let v = this.data[index].value[i];
2501 if (Number.isNaN(v))
2511 // array could be empty (no data), so min/max would be NaN
2512 if (Number.isNaN(minValue) || Number.isNaN(maxValue))
2513 minValue = maxValue = 0;
2515 if (this.autoscaleMin)
2516 this.yMin0 = this.yMin = minValue;
2517 if (this.autoscaleMax)
2518 this.yMax0 = this.yMax = maxValue;
2520 if (minValue === undefined || maxValue === undefined) {
2525 if (this.yMin0 === this.yMax0) {
2530 if (this.yMax0 < this.yMin0)
2531 this.yMax0 = this.yMin0 + 1;
2534 if (this.autoscaleMin) {
2536 this.yMin = 0.8 * this.yMin0;
2538 // leave 10% space below graph
2539 this.yMin = this.yMin0 - this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2541 this.yMin = this.yMin0;
2542 if (this.logAxis && this.yMin <= 0)
2545 if (this.autoscaleMax) {
2547 this.yMax = 1.2 * this.yMax0;
2549 // leave 10% space above graph
2550 this.yMax = this.yMax0 + this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2552 this.yMax = this.yMax0;
2553 if (this.logAxis && this.yMax <= 0)
2557 this.yMax = this.truncateInfinity(this.yMax)
2558 this.yMin = this.truncateInfinity(this.yMin)
2561function convertLastWritten(last) {
2563 return "no data available";
2565 let d = new Date(last * 1000).toLocaleDateString(
2567 day: '2-digit', month: 'short', year: '2-digit',
2568 hour12: false, hour: '2-digit', minute: '2-digit'
2572 return "last data: " + d;
2575MhistoryGraph.prototype.updateURL = function() {
2576 let url = window.location.href;
2577 if (url.search("&A=") !== -1)
2578 url = url.slice(0, url.search("&A="));
2579 url += "&A=" + Math.round(this.tMin) + "&B=" + Math.round(this.tMax);
2581 if (url !== window.location.href)
2582 window.history.replaceState({}, "Midas History", url);
2585function createPinstripeCanvas() {
2586 const patternCanvas = document.createElement("canvas");
2587 const pctx = patternCanvas.getContext('2d', { antialias: true });
2588 const colour = "#FFC0C0";
2590 const CANVAS_SIDE_LENGTH = 90;
2591 const WIDTH = CANVAS_SIDE_LENGTH;
2592 const HEIGHT = CANVAS_SIDE_LENGTH;
2593 const DIVISIONS = 4;
2595 patternCanvas.width = WIDTH;
2596 patternCanvas.height = HEIGHT;
2597 pctx.fillStyle = colour;
2601 pctx.moveTo(0, HEIGHT * (1 / DIVISIONS));
2602 pctx.lineTo(WIDTH * (1 / DIVISIONS), 0);
2604 pctx.lineTo(0, HEIGHT * (1 / DIVISIONS));
2609 pctx.moveTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2610 pctx.lineTo(WIDTH * (1 / DIVISIONS), HEIGHT);
2611 pctx.lineTo(0, HEIGHT);
2612 pctx.lineTo(0, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2613 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), 0);
2614 pctx.lineTo(WIDTH, 0);
2615 pctx.lineTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2620 pctx.moveTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2621 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), HEIGHT);
2622 pctx.lineTo(WIDTH, HEIGHT);
2623 pctx.lineTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2626 return patternCanvas;
2629MhistoryGraph.prototype.draw = function () {
2632 // draw maximal 30 times per second
2633 if (!this.forceRedraw) {
2634 if (new Date().getTime() < this.lastDrawTime + 30)
2636 this.lastDrawTime = new Date().getTime();
2638 this.forceRedraw = false;
2640 let update_last_written = false;
2642 let ctx = this.canvas.getContext("2d");
2644 ctx.fillStyle = this.color.background;
2645 ctx.fillRect(0, 0, this.width, this.height);
2647 if (this.data === undefined) {
2649 ctx.font = "14px sans-serif";
2650 ctx.strokeStyle = "#808080";
2651 ctx.fillStyle = "#808080";
2652 ctx.textAlign = "center";
2653 ctx.textBaseline = "middle";
2654 ctx.fillText("Data being loaded ...", this.width / 2, this.height / 2);
2659 ctx.font = "14px sans-serif";
2661 if (this.height === undefined || this.width === undefined)
2663 if (this.yMin === undefined || Number.isNaN(this.yMin))
2665 if (this.yMax === undefined || Number.isNaN(this.yMax))
2668 let axisLabelWidth = this.drawVAxis(ctx, 50, this.height - 25, this.height - 35,
2669 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.logAxis, false);
2671 if (axisLabelWidth === undefined)
2674 if (this.showAxis) {
2675 this.x1 = axisLabelWidth + 15;
2676 this.y1 = this.height - 25;
2677 this.x2 = this.width - 26;
2681 this.y1 = this.height - 1;
2682 this.x2 = this.width - 26;
2686 if (this.showMenuButtons === false)
2687 this.x2 = this.width - 1;
2690 if (!this.floating && // suppress title since this is already in the dialog box
2693 ctx.strokeStyle = this.color.axis;
2694 ctx.fillStyle = "#F0F0F0";
2695 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, 20);
2696 ctx.fillRect(this.x1, 6, this.x2 - this.x1, 20);
2697 ctx.textAlign = "center";
2698 ctx.textBaseline = "middle";
2699 ctx.fillStyle = "#808080";
2701 if (this.group !== undefined)
2702 str += this.group + " - " + this.panel;
2703 else if (this.historyVar !== undefined)
2704 str += this.historyVar;
2706 if (this.debugString !== "")
2707 str += " - " + this.debugString;
2709 ctx.fillText(str, (this.x2 + this.x1) / 2, 16);
2712 let s = Math.round(this.binSize);
2713 ctx.textAlign = "right";
2714 ctx.fillText(s, this.x2 - 10, 16);
2717 // re-calculate axis scaling since x2, y2 might have been changed above
2718 this.timeToXInit(); // initialize scale factor t -> x
2719 this.valueToYInit(); // initialize scale factor v -> y
2722 ctx.strokeStyle = this.color.axis;
2723 ctx.drawLine(this.x1, this.y2, this.x2, this.y2);
2724 ctx.drawLine(this.x2, this.y2, this.x2, this.y1);
2726 if (this.logAxis && this.yMin < 1E-20)
2728 if (this.logAxis && this.yMax < 1E-18)
2730 this.drawVAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
2731 -4, -7, -10, -12, this.x2 - this.x1, this.yMin, this.yMax, this.logAxis, true);
2732 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
2733 4, 7, 10, 10, this.y2 - this.y1, this.tMin, this.tMax);
2735 // draw hatched area for "future"
2736 let t = new Date() / 1000;
2737 if (this.tMax > t) {
2738 let x = this.timeToX(t);
2741 ctx.fillStyle = ctx.createPattern(createPinstripeCanvas(), 'repeat');
2742 ctx.fillRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2744 ctx.strokeStyle = this.color.axis;
2745 ctx.strokeRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2748 // determine precision
2750 if (this.yMin === 0)
2753 n_sig1 = Math.floor(Math.log(Math.abs(this.yMin)) / Math.log(10)) -
2754 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2756 if (this.yMax === 0)
2759 n_sig2 = Math.floor(Math.log(Math.abs(this.yMax)) / Math.log(10)) -
2760 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2762 n_sig1 = Math.max(n_sig1, n_sig2);
2763 n_sig1 = Math.max(1, n_sig1);
2765 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2766 if (Math.abs(this.yMin) < 100000)
2767 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMin)) /
2768 Math.log(10) + 0.001) + 1);
2769 if (Math.abs(this.yMax) < 100000)
2770 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMax)) /
2771 Math.log(10) + 0.001) + 1);
2776 this.yPrecision = Math.max(6, n_sig1); // use at least 5 digits
2780 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2783 //profile("drawinit");
2786 for (let di = 0; di < this.data.length; di++)
2787 nPoints += this.data[di].time.length;
2789 // convert values to points if window has changed or number of points have changed
2790 if (this.tMin !== this.tMinOld || this.tMax !== this.tMaxOld ||
2791 this.yMin !== this.yMinOld || this.yMax !== this.yMaxOld ||
2792 nPoints !== this.nPointsOld || this.forceConvert) {
2794 this.tMinOld = this.tMin;
2795 this.tMaxOld = this.tMax;
2796 this.yMinOld = this.yMin;
2797 this.yMaxOld = this.yMax;
2798 this.nPointsOld = nPoints;
2799 this.forceConvert = false;
2802 for (let di = 0; di < this.data.length; di++) {
2804 if (this.x[di] === undefined) {
2805 this.x[di] = []; // x/y contain visible part of graph
2807 this.t[di] = []; // t/v contain time/value pairs corresponding to x/y
2810 this.vRaw[di] = []; // vRaw contains the value before the formula
2815 if (this.data[di].time.length === 0)
2818 let i1 = binarySearch(this.data[di].time, this.tMin);
2820 i1--; // add point to the left
2821 let i2 = binarySearch(this.data[di].time, this.tMax);
2822 if (i2 < this.data[di].time.length - 1)
2823 i2++; // add points to the right
2826 if (!this.binned || this.events[di] === "Run transitions") {
2827 for (let i = i1; i <= i2; i++) {
2828 let x = this.timeToX(this.data[di].time[i]);
2829 let y = this.valueToY(this.data[di].value[i]);
2834 if (!Number.isNaN(y)) {
2837 this.t[di][n] = this.data[di].time[i];
2838 this.v[di][n] = this.data[di].value[i];
2839 if (this.data[di].rawValue)
2840 this.vRaw[di][n] = this.data[di].rawValue[i];
2845 // truncate arrays if now shorter
2846 this.x[di].length = n;
2847 this.y[di].length = n;
2848 this.t[di].length = n;
2849 this.v[di].length = n;
2850 if (this.data[di].rawValue)
2851 this.vRaw[di].length = n;
2856 for (let i = i1; i <= i2; i++) {
2858 if (this.data[di].bin[i].count === 0)
2862 p.n = this.data[di].bin[i].count;
2863 p.x = Math.round(this.timeToX(this.data[di].time[i]));
2864 p.t = this.data[di].time[i];
2866 p.first = this.valueToY(this.data[di].bin[i].firstValue);
2867 p.min = this.valueToY(this.data[di].bin[i].minValue);
2868 p.minValue = this.data[di].bin[i].minValue;
2869 p.max = this.valueToY(this.data[di].bin[i].maxValue);
2870 p.maxValue = this.data[di].bin[i].maxValue;
2871 p.last = this.valueToY(this.data[di].bin[i].lastValue);
2873 if (this.data[di].binRaw) {
2874 p.rawFirstValue = this.data[di].binRaw[i].firstValue;
2875 p.rawMinValue = this.data[di].binRaw[i].minValue;
2876 p.rawMaxValue = this.data[di].binRaw[i].maxValue;
2877 p.rawLastValue = this.data[di].binRaw[i].lastValue;
2882 this.x[di][n] = p.x;
2887 // truncate arrays if now shorter
2888 this.p[di].length = n;
2889 this.x[di].length = n;
2890 if (this.data[di].rawValue)
2891 this.vRaw[di].length = n;
2896 // draw shaded areas
2897 if (this.showFill) {
2898 for (let di = 0; di < this.data.length; di++) {
2899 if (this.solo.active && this.solo.index !== di)
2902 if (this.events[di] === "Run transitions")
2905 ctx.fillStyle = this.param["Colour"][di];
2907 // don't draw lines over "gaps"
2908 let gap = this.timeToXScale * 600; // 10 min
2910 gap = 5; // minimum of 5 pixels
2913 if (this.p[di].length > 0) {
2914 let p = this.p[di][0];
2919 ctx.moveTo(p.x, p.first);
2920 ctx.lineTo(p.x, p.last);
2921 for (let i = 1; i < this.p[di].length; i++) {
2923 if (p.x - xold < gap) {
2924 ctx.lineTo(p.x, p.first);
2925 ctx.lineTo(p.x, p.last);
2927 ctx.lineTo(xold, this.valueToY(0));
2928 ctx.lineTo(p.x, this.valueToY(0));
2929 ctx.lineTo(p.x, p.first);
2930 ctx.lineTo(p.x, p.last);
2934 ctx.lineTo(xold, this.valueToY(0));
2935 ctx.lineTo(x0, this.valueToY(0));
2937 ctx.globalAlpha = 0.1;
2939 ctx.globalAlpha = 1;
2942 if (this.x[di].length > 0 && this.y[di].length > 0) {
2943 let x = this.x[di][0];
2944 let y = this.y[di][0];
2950 for (let i = 1; i < this.x[di].length; i++) {
2956 ctx.lineTo(xold, this.valueToY(0));
2957 ctx.lineTo(x, this.valueToY(0));
2962 ctx.lineTo(xold, this.valueToY(0));
2963 ctx.lineTo(x0, this.valueToY(0));
2965 ctx.globalAlpha = 0.1;
2967 ctx.globalAlpha = 1;
2973 // profile("Draw shaded areas");
2976 for (let di = 0; di < this.data.length; di++) {
2977 if (this.solo.active && this.solo.index !== di)
2980 if (this.events[di] === "Run transitions") {
2982 if (this.tags[di] === "State") {
2983 if (this.x[di].length < 200) {
2984 for (let i = 0; i < this.x[di].length; i++) {
2985 if (this.v[di][i] === 1) {
2986 ctx.strokeStyle = "#FF0000";
2987 ctx.fillStyle = "#808080";
2988 ctx.textAlign = "right";
2989 ctx.textBaseline = "top";
2990 ctx.fillText(this.v[di + 1][i], this.x[di][i] - 5, this.y2 + 3);
2991 } else if (this.v[di][i] === 3) {
2992 ctx.strokeStyle = "#00A000";
2993 ctx.fillStyle = "#808080";
2994 ctx.textAlign = "left";
2995 ctx.textBaseline = "top";
2996 ctx.fillText(this.v[di + 1][i], this.x[di][i] + 3, this.y2 + 3);
2998 ctx.strokeStyle = "#F9A600";
3001 ctx.setLineDash([8, 2]);
3002 ctx.drawLine(Math.floor(this.x[di][i]), this.y1, Math.floor(this.x[di][i]), this.y2);
3003 ctx.setLineDash([]);
3010 ctx.strokeStyle = this.param["Colour"][di];
3012 // don't draw lines over "gaps"
3013 let gap = this.timeToXScale * 600; // 10 min
3015 gap = 5; // minimum of 5 pixels
3018 if (this.p[di].length > 0) {
3019 let p = this.p[di][0];
3020 //console.log("di:" + di + " i:" + 0 + " x:" + p.x, " y:" + p.first);
3023 ctx.moveTo(p.x, p.first);
3024 ctx.lineTo(p.x, p.max + 1); // in case min==max
3025 ctx.lineTo(p.x, p.min);
3026 ctx.lineTo(p.x, p.last);
3027 for (let i = 1; i < this.p[di].length; i++) {
3029 //console.log("di:" + di + " i:" + i + " x:" + p.x, " y:" + p.first);
3030 if (p.x - xold < gap) {
3031 // draw lines first - max - min - last
3032 ctx.lineTo(p.x, p.first);
3033 ctx.lineTo(p.x, p.max + 1); // in case min==max
3034 ctx.lineTo(p.x, p.min);
3035 ctx.lineTo(p.x, p.last);
3036 } else { // don't draw gap
3037 // draw lines first - max - min - last
3038 ctx.moveTo(p.x, p.first);
3039 ctx.lineTo(p.x, p.max + 1); // in case min==max
3040 ctx.lineTo(p.x, p.min);
3041 ctx.lineTo(p.x, p.last);
3048 if (this.x[di].length === 1) {
3049 let x = this.x[di][0];
3050 let y = this.y[di][0];
3051 ctx.fillStyle = this.param["Colour"][di];
3052 ctx.fillRect(x - 1, y - 1, 3, 3);
3054 if (this.x[di].length > 0) {
3056 let x = this.x[di][0];
3057 let y = this.y[di][0];
3060 for (let i = 1; i < this.x[di].length; i++) {
3061 let x = this.x[di][i];
3062 let y = this.y[di][i];
3076 ctx.restore(); // remove clipping
3078 // profile("Draw graphs");
3080 // labels with variable names and values
3081 if (this.showLabels) {
3082 if (this.solo.active)
3083 this.variablesHeight = 17 + 7;
3085 this.variablesHeight = this.param["Variables"].length * 17 + 7;
3086 this.variablesWidth = 0;
3088 // determine width of widest label
3089 this.param["Variables"].forEach((v, i) => {
3091 if (this.param.Label[i] !== "") {
3092 width = ctx.measureText(this.param.Label[i]).width;
3094 width = ctx.measureText(splitEventAndTagName(v)[1]).width;
3097 if (this.param["Show raw value"] !== undefined &&
3098 this.param["Show raw value"][i])
3099 width += ctx.measureText(" (Raw)").width;
3101 width += 20; // space between name and value
3103 if (this.v[i] !== undefined && this.v[i].length > 0) {
3104 // use last point in array
3105 let index = this.v[i].length - 1;
3107 // use point at current marker
3108 if (this.marker.active)
3109 index = this.marker.index;
3111 if (index < this.v[i].length) {
3113 if (this.param["Show raw value"] !== undefined &&
3114 this.param["Show raw value"][i])
3115 value = this.vRaw[i][index];
3117 value = this.v[i][index];
3119 // convert value to string with 6 digits
3120 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3121 width += ctx.measureText(str).width;
3123 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3124 // use last point in array
3125 let index = this.p[i].length - 1;
3127 // use point at current marker
3128 if (this.marker.active)
3129 index = this.marker.index;
3131 if (index < this.p[i].length) {
3133 if (this.param["Show raw value"] !== undefined &&
3134 this.param["Show raw value"][i])
3135 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3137 value = (this.p[i][index].minValue + this.p[i][index].maxValue)/2;
3139 // convert value to string with 6 digits
3140 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3141 width += ctx.measureText(str).width;
3144 width += ctx.measureText(convertLastWritten(this.lastWritten[i])).width;
3147 this.variablesWidth = Math.max(this.variablesWidth, width);
3150 let xLabel = this.x1;
3151 if (this.solo.active)
3152 xLabel = this.x1 + 28;
3156 ctx.rect(xLabel, this.y2, 25 + this.variablesWidth + 7, this.variablesHeight + 2);
3159 ctx.strokeStyle = this.color.axis;
3160 ctx.fillStyle = "#F0F0F0";
3161 ctx.globalAlpha = 0.5;
3162 ctx.strokeRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3163 ctx.fillRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3164 ctx.globalAlpha = 1;
3166 this.param["Variables"].forEach((v, i) => {
3168 if (this.solo.active && i !== this.solo.index)
3172 if (!this.solo.active)
3176 ctx.strokeStyle = this.param["Colour"][i];
3177 ctx.drawLine(xLabel + 5, this.y2 + 14 + yLabel, xLabel + 20, this.y2 + 14 + yLabel);
3180 ctx.textAlign = "left";
3181 ctx.textBaseline = "middle";
3182 ctx.fillStyle = "#404040";
3185 if (this.param.Label[i] !== "")
3186 str = this.param.Label[i];
3188 str = splitEventAndTagName(v)[1];
3190 if (this.param["Show raw value"] !== undefined &&
3191 this.param["Show raw value"][i])
3194 ctx.fillText(str, xLabel + 25, this.y2 + 14 + yLabel);
3196 ctx.textAlign = "right";
3199 if (this.v[i] !== undefined && this.v[i].length > 0) {
3200 // use last point in array
3201 let index = this.v[i].length - 1;
3203 // use point at current marker
3204 if (this.marker.active)
3205 index = this.marker.index;
3207 if (index < this.v[i].length) {
3208 // convert value to string with 6 digits
3210 if (this.param["Show raw value"] !== undefined &&
3211 this.param["Show raw value"][i])
3212 value = this.vRaw[i][index];
3214 value = this.v[i][index];
3215 let str = value.toPrecision(this.yPrecision).stripZeros();
3216 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3218 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3222 // use last point in array
3223 let index = this.p[i].length - 1;
3225 // use point at current marker
3226 if (this.marker.active)
3227 index = this.marker.index;
3229 if (index < this.p[i].length) {
3230 // convert value to string with 6 digits
3232 if (this.param["Show raw value"] !== undefined &&
3233 this.param["Show raw value"][i])
3234 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3236 value = (this.p[i][index].minValue + this.p[i][index].maxValue) / 2;
3237 let str = value.toPrecision(this.yPrecision).stripZeros();
3238 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3243 if (this.lastWritten.length > 0) {
3244 if (this.lastWritten[i] > this.tMax) {
3245 //console.log("last written is in the future: " + this.events[i] + ", lw: " + this.lastWritten[i], ", this.tMax: " + this.tMax, ", diff: " + (this.lastWritten[i] - this.tMax));
3246 update_last_written = true;
3248 ctx.fillText(convertLastWritten(this.lastWritten[i]),
3249 xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3251 //console.log("last_written was not loaded yet");
3252 update_last_written = true;
3258 ctx.restore(); // remove clipping
3261 // "updating" notice
3262 if (this.pendingUpdates > 0) {
3263 let str = "Updating data ...";
3264 ctx.strokeStyle = "#404040";
3265 ctx.fillStyle = "#FFC0C0";
3266 ctx.fillRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3267 ctx.strokeRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3268 ctx.fillStyle = "#404040";
3269 ctx.textAlign = "left";
3270 ctx.textBaseline = "middle";
3271 ctx.fillText(str, this.x1 + 10, this.y1 - 13);
3276 for (let i = 0; i < this.data.length; i++) {
3277 if (this.data[i].time === undefined || this.data[i].time.length === 0) {
3283 // "empty window" notice
3284 if (no_data && this.pendingUpdates === 0) {
3285 ctx.font = "16px sans-serif";
3286 let str = "No data available";
3287 ctx.strokeStyle = "#404040";
3288 ctx.fillStyle = "#F0F0F0";
3289 let w = ctx.measureText(str).width + 10;
3291 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3292 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3293 ctx.fillStyle = "#404040";
3294 ctx.textAlign = "center";
3295 ctx.textBaseline = "middle";
3296 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
3297 ctx.font = "14px sans-serif";
3301 if (this.showMenuButtons) {
3303 let buttonSize = 20;
3304 this.button.forEach(b => {
3305 b.x1 = this.width - buttonSize - 6;
3306 b.y1 = 6 + y * (buttonSize + 4);
3307 b.width = buttonSize + 4;
3308 b.height = buttonSize + 4;
3311 if (b.src === "maximize-2.svg") {
3312 let s = window.location.href;
3313 if (s.indexOf("&A") > -1)
3314 s = s.substr(0, s.indexOf("&A"));
3315 if (s === encodeURI(this.baseURL + "&group=" + this.group + "&panel=" + this.panel)) {
3321 if (b.src === "corner-down-left.svg") {
3324 if (this.solo.active)
3332 if (b.src === "play.svg" && !this.scroll)
3333 ctx.fillStyle = "#FFC0C0";
3335 ctx.fillStyle = "#F0F0F0";
3336 ctx.strokeStyle = "#808080";
3337 ctx.fillRect(b.x1, b.y1, b.width, b.height);
3338 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
3339 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
3346 if (this.showZoomButtons) {
3348 if (this.showMenuButtons)
3349 xb = this.width - 26 - 40;
3351 xb = this.width - 41;
3352 let yb = this.y1 - 20;
3353 ctx.fillStyle = "#F0F0F0";
3354 ctx.globalAlpha = 0.5;
3355 ctx.fillRect(xb, yb, 20, 20);
3356 ctx.globalAlpha = 1;
3357 ctx.strokeStyle = "#808080";
3358 ctx.strokeRect(xb, yb, 20, 20);
3359 ctx.strokeStyle = "#202020";
3360 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3361 ctx.drawLine(xb + 10, yb + 4, xb + 10, yb + 17);
3364 ctx.globalAlpha = 0.5;
3365 ctx.fillRect(xb, yb, 20, 20);
3366 ctx.globalAlpha = 1;
3367 ctx.strokeStyle = "#808080";
3368 ctx.strokeRect(xb, yb, 20, 20);
3369 ctx.strokeStyle = "#202020";
3370 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3374 if (this.zoom.x.active) {
3375 ctx.fillStyle = "#808080";
3376 ctx.globalAlpha = 0.2;
3377 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
3378 ctx.globalAlpha = 1;
3379 ctx.strokeStyle = "#808080";
3380 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
3381 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
3383 if (this.zoom.y.active) {
3384 ctx.fillStyle = "#808080";
3385 ctx.globalAlpha = 0.2;
3386 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
3387 ctx.globalAlpha = 1;
3388 ctx.strokeStyle = "#808080";
3389 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
3390 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
3394 if (this.marker.active) {
3398 ctx.globalAlpha = 0.1;
3399 ctx.arc(this.marker.x, this.marker.y, 10, 0, 2 * Math.PI);
3400 ctx.fillStyle = "#000000";
3402 ctx.globalAlpha = 1;
3405 ctx.arc(this.marker.x, this.marker.y, 4, 0, 2 * Math.PI);
3406 ctx.fillStyle = "#000000";
3409 ctx.strokeStyle = "#A0A0A0";
3410 ctx.drawLine(this.marker.x, this.y1, this.marker.x, this.y2);
3413 let v = this.marker.v;
3416 if (this.param.Label[this.marker.graphIndex] !== "")
3417 s = this.param.Label[this.marker.graphIndex];
3419 s = this.param["Variables"][this.marker.graphIndex];
3421 if (this.param["Show raw value"] !== undefined &&
3422 this.param["Show raw value"][this.marker.graphIndex])
3425 s += ": " + v.toPrecision(this.yPrecision).stripZeros();
3427 let w = ctx.measureText(s).width + 6;
3428 let h = ctx.measureText("M").width * 1.2 + 6;
3429 let x = this.marker.mx + 20;
3430 let y = this.marker.my + h / 3 * 2;
3434 if (x + w >= this.x2) {
3435 x = this.marker.x - 20 - w;
3439 if (y > (this.y1 - this.y2) / 2) {
3440 y = this.marker.y - h / 3 * 5;
3444 ctx.strokeStyle = "#808080";
3445 ctx.fillStyle = "#F0F0F0";
3446 ctx.textBaseline = "middle";
3447 ctx.fillRect(x, y, w, h);
3448 ctx.strokeRect(x, y, w, h);
3449 ctx.fillStyle = "#404040";
3450 ctx.fillText(s, x + 3, y + h / 2);
3453 ctx.strokeStyle = "#808080";
3454 ctx.drawLine(this.marker.x, this.marker.y, xl, yl);
3457 s = timeToLabel(this.marker.t, 1, true);
3458 w = ctx.measureText(s).width + 10;
3459 h = ctx.measureText("M").width * 1.2 + 11;
3460 x = this.marker.x - w / 2;
3464 if (x + w >= this.x2)
3467 ctx.strokeStyle = "#808080";
3468 ctx.fillStyle = "#F0F0F0";
3469 ctx.fillRect(x, y, w, h);
3470 ctx.strokeRect(x, y, w, h);
3471 ctx.fillStyle = "#404040";
3472 ctx.fillText(s, x + 5, y + h / 2);
3475 this.lastDrawTime = new Date().getTime();
3477 // profile("Finished draw");
3479 if (update_last_written) {
3480 this.updateLastWritten();
3484 if (this.updateURLTimer !== undefined)
3485 window.clearTimeout(this.updateURLTimer);
3487 if (this.plotIndex === 0 && this.floating !== true)
3488 this.updateURLTimer = window.setTimeout(this.updateURL.bind(this), 10);
3491MhistoryGraph.prototype.drawVAxis = function (ctx, x1, y1, height, minor, major,
3492 text, label, grid, ymin, ymax, logaxis, draw) {
3493 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
3494 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
3495 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
3498 ctx.textAlign = "right";
3500 ctx.textAlign = "left";
3501 ctx.textBaseline = "middle";
3502 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
3504 if (ymax <= ymin || height <= 0)
3507 if (!isFinite(ymax - ymin) || ymax == Number.MAX_VALUE) {
3508 dy = Number.MAX_VALUE / 10;
3512 } else if (logaxis) {
3513 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
3522 // use 6 as min tick distance
3523 dy = (ymax - ymin) / (height / 6);
3525 int_dy = Math.floor(Math.log(dy) / Math.log(10));
3526 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
3533 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
3534 major_base = label_base = tick_base + 1;
3536 // rounding up of dy, label_dy
3537 dy = Math.pow(10, int_dy) * base[tick_base];
3538 major_dy = Math.pow(10, int_dy) * base[major_base];
3539 label_dy = major_dy;
3541 // number of significant digits
3545 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
3546 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3551 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
3552 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3554 n_sig1 = Math.max(n_sig1, n_sig2);
3555 n_sig1 = Math.max(1, n_sig1);
3557 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
3558 if (Math.abs(ymin) < 100000)
3559 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
3560 Math.log(10) + 0.001) + 1);
3561 if (Math.abs(ymax) < 100000)
3562 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
3563 Math.log(10) + 0.001) + 1);
3565 // increase label_dy if labels would overlap
3566 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
3568 label_dy = Math.pow(10, int_dy) * base[label_base];
3569 if (label_base % 3 === 2 && major_base % 3 === 1) {
3571 major_dy = Math.pow(10, int_dy) * base[major_base];
3576 y_act = Math.floor(ymin / dy) * dy;
3578 let last_label_y = y1;
3582 ctx.strokeStyle = this.color.axis;
3583 ctx.drawLine(x1, y1, x1, y1 - height);
3588 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
3589 (Math.log(ymax) - Math.log(ymin)) * height;
3590 else if (!(isFinite(ymax - ymin)))
3591 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
3593 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
3594 ys = Math.round(y_screen);
3596 if (y_screen < y1 - height - 0.001 || isNaN(ys))
3599 if (y_screen <= y1 + 0.001) {
3600 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
3601 dy / major_dy / 10.0) {
3603 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
3604 dy / label_dy / 10.0) {
3607 ctx.strokeStyle = this.color.axis;
3608 ctx.drawLine(x1, ys, x1 + text, ys);
3612 if (grid !== 0 && ys < y1 && ys > y1 - height)
3614 ctx.strokeStyle = this.color.grid;
3615 ctx.drawLine(x1, ys, x1 + grid, ys);
3621 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3622 str = y_act.toExponential(n_sig1).stripZeros();
3624 str = y_act.toPrecision(n_sig1).stripZeros();
3625 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3627 ctx.strokeStyle = this.color.label;
3628 ctx.fillStyle = this.color.label;
3629 ctx.fillText(str, x1 + label, ys);
3631 last_label_y = ys - textHeight / 2;
3636 ctx.strokeStyle = this.color.axis;
3637 ctx.drawLine(x1, ys, x1 + major, ys);
3641 if (grid !== 0 && ys < y1 && ys > y1 - height)
3643 ctx.strokeStyle = this.color.grid;
3644 ctx.drawLine(x1, ys, x1 + grid, ys);
3657 ctx.strokeStyle = this.color.axis;
3658 ctx.drawLine(x1, ys, x1 + minor, ys);
3661 // for logaxis, also put labels on minor tick marks
3665 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3666 str = y_act.toExponential(n_sig1).stripZeros();
3668 str = y_act.toPrecision(n_sig1).stripZeros();
3669 if (ys - textHeight / 2 > y1 - height &&
3670 ys + textHeight / 2 < y1 &&
3671 ys + textHeight < last_label_y + 2) {
3672 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3674 ctx.strokeStyle = this.color.label;
3675 ctx.fillStyle = this.color.label;
3676 ctx.fillText(str, x1 + label, ys);
3687 // suppress 1.23E-17 ...
3688 if (Math.abs(y_act) < dy / 100)
3698 day: '2-digit', month: 'short', year: '2-digit',
3699 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3704 day: '2-digit', month: 'short', year: '2-digit',
3705 hour12: false, hour: '2-digit', minute: '2-digit'
3710 day: '2-digit', month: 'short', year: '2-digit',
3711 hour12: false, hour: '2-digit', minute: '2-digit'
3716 day: '2-digit', month: 'short', year: '2-digit'
3721 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3726 hour12: false, hour: '2-digit', minute: '2-digit'
3731 hour12: false, hour: '2-digit', minute: '2-digit'
3736 day: '2-digit', month: 'short', year: '2-digit',
3737 hour12: false, hour: '2-digit', minute: '2-digit'
3742 day: '2-digit', month: 'short', year: '2-digit'
3745function timeToLabel(sec, base, forceDate) {
3746 let d = mhttpd_get_display_time(sec).date;
3750 return d.toLocaleTimeString('en-GB', options1);
3751 } else if (base < 600) {
3752 return d.toLocaleTimeString('en-GB', options2);
3753 } else if (base < 3600 * 24) {
3754 return d.toLocaleTimeString('en-GB', options3);
3756 return d.toLocaleDateString('en-GB', options4);
3761 return d.toLocaleTimeString('en-GB', options5);
3762 } else if (base < 600) {
3763 return d.toLocaleTimeString('en-GB', options6);
3764 } else if (base < 3600 * 3) {
3765 return d.toLocaleTimeString('en-GB', options7);
3766 } else if (base < 3600 * 24) {
3767 return d.toLocaleTimeString('en-GB', options8);
3769 return d.toLocaleDateString('en-GB', options9);
3773MhistoryGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
3774 text, label, grid, xmin, xmax) {
3775 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
3776 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
3778 ctx.textAlign = "left";
3779 ctx.textBaseline = "top";
3781 if (xmax <= xmin || width <= 0)
3784 /* force date display if xmax not today */
3785 let d1 = new Date(xmax * 1000);
3786 let d2 = new Date();
3787 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
3789 /* use 5 pixel as min tick distance */
3790 let dx = Math.round((xmax - xmin) / (width / 5));
3793 for (tick_base = 0; base[tick_base]; tick_base++) {
3794 if (base[tick_base] > dx)
3797 if (!base[tick_base])
3799 dx = base[tick_base];
3801 let major_base = tick_base;
3804 let label_base = major_base;
3808 let str = timeToLabel(xmin, label_dx, forceDate);
3809 let maxwidth = ctx.measureText(str).width;
3811 /* increasing label_dx, if labels would overlap */
3812 if (maxwidth > 0.75 * label_dx / (xmax - xmin) * width) {
3813 if (base[label_base + 1])
3814 label_dx = base[++label_base];
3816 label_dx += 3600 * 24;
3818 if (label_base > major_base + 1 || !base[label_base + 1]) {
3819 if (base[major_base + 1])
3820 major_dx = base[++major_base];
3822 major_dx += 3600 * 24;
3825 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3826 if (base[tick_base + 1])
3827 dx = base[++tick_base];
3836 let d = new Date(xmin * 1000);
3837 let tz = d.getTimezoneOffset() * 60;
3839 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3841 ctx.strokeStyle = this.color.axis;
3842 ctx.drawLine(x1, y1, x1 + width, y1);
3845 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3847 if (xs > x1 + width + 0.001)
3851 if ((x_act - tz) % major_dx === 0) {
3852 if ((x_act - tz) % label_dx === 0) {
3854 ctx.strokeStyle = this.color.axis;
3855 ctx.drawLine(xs, y1, xs, y1 + text);
3858 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3859 ctx.strokeStyle = this.color.grid;
3860 ctx.drawLine(xs, y1, xs, y1 + grid);
3865 let str = timeToLabel(x_act, label_dx, forceDate);
3867 // if labels at edge, shift them in
3868 let xl = xs - ctx.measureText(str).width / 2;
3871 if (xl + ctx.measureText(str).width >= xr)
3872 xl = xr - ctx.measureText(str).width - 1;
3873 ctx.strokeStyle = this.color.label;
3874 ctx.fillStyle = this.color.label;
3875 ctx.fillText(str, xl, y1 + label);
3879 ctx.strokeStyle = this.color.axis;
3880 ctx.drawLine(xs, y1, xs, y1 + major);
3884 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3885 ctx.strokeStyle = this.color.grid;
3886 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
3890 ctx.strokeStyle = this.color.axis;
3891 ctx.drawLine(xs, y1, xs, y1 + minor);
3900MhistoryGraph.prototype.download = function (mode) {
3902 let leftDate = mhttpd_get_display_time(this.tMin).date;
3903 let rightDate = mhttpd_get_display_time(this.tMax).date;
3904 let filename = this.group + "-" + this.panel + "-" +
3905 leftDate.getFullYear() +
3906 ("0" + (leftDate.getUTCMonth() + 1)).slice(-2) +
3907 ("0" + leftDate.getUTCDate()).slice(-2) + "-" +
3908 ("0" + leftDate.getUTCHours()).slice(-2) +
3909 ("0" + leftDate.getUTCMinutes()).slice(-2) +
3910 ("0" + leftDate.getUTCSeconds()).slice(-2) + "-" +
3911 rightDate.getFullYear() +
3912 ("0" + (rightDate.getUTCMonth() + 1)).slice(-2) +
3913 ("0" + rightDate.getUTCDate()).slice(-2) + "-" +
3914 ("0" + rightDate.getUTCHours()).slice(-2) +
3915 ("0" + rightDate.getUTCMinutes()).slice(-2) +
3916 ("0" + rightDate.getUTCSeconds()).slice(-2);
3918 // use trick from FileSaver.js
3919 let a = document.getElementById('downloadHook');
3921 a = document.createElement("a");
3922 a.style.display = "none";
3923 a.id = "downloadHook";
3924 document.body.appendChild(a);
3927 if (mode === "CSV") {
3931 this.param["Variables"].forEach(v => {
3934 data += v + " MIN," + v + " MAX,";
3938 data = data.slice(0, -1);
3942 let nvar = this.param["Variables"].length;
3943 for (let index=0 ; index < nvar ; index++)
3944 if (this.data[index].time.length > maxlen)
3945 maxlen = this.data[index].time.length;
3947 for (let di=0 ; di < nvar ; di++)
3948 for (let i = 0; i < maxlen; i++) {
3949 if (i < this.data[di].time.length &&
3950 this.data[di].time[i] > this.tMin) {
3956 for (let i = 0; i < maxlen; i++) {
3958 for (let di = 0 ; di < nvar ; di++) {
3959 if (index[di] < this.data[di].time.length &&
3960 this.data[di].time[index[di]] > this.tMin && this.data[di].time[index[di]] < this.tMax) {
3962 l += this.data[di].time[index[di]] + ",";
3964 if (this.param["Show raw value"] !== undefined &&
3965 this.param["Show raw value"][di]) {
3966 l += this.data[di].binRaw[index[di]].minValue + ",";
3967 l += this.data[di].binRaw[index[di]].maxValue + ",";
3969 l += this.data[di].bin[index[di]].minValue + ",";
3970 l += this.data[di].bin[index[di]].maxValue + ",";
3975 l += this.data[di].time[index[di]] + ",";
3977 if (this.param["Show raw value"] !== undefined &&
3978 this.param["Show raw value"][di])
3979 l += this.data[di].rawValue[index[di]] + ",";
3981 l += this.data[di].value[index[di]] + ",";
3988 if (l.split(',').some(s => s)) { // don't add if only commas
3989 l = l.slice(0, -1); // remove last comma
3994 let blob = new Blob([data], {type: "text/csv"});
3995 let url = window.URL.createObjectURL(blob);
3998 a.download = filename;
4000 window.URL.revokeObjectURL(url);
4001 dlgAlert("Data downloaded to '" + filename + "'");
4003 } else if (mode === "PNG") {
4006 this.showZoomButtons = false;
4007 this.showMenuButtons = false;
4008 this.forceRedraw = true;
4009 this.forceConvert = true;
4013 this.canvas.toBlob(function (blob) {
4014 let url = window.URL.createObjectURL(blob);
4017 a.download = filename;
4019 window.URL.revokeObjectURL(url);
4020 dlgAlert("Image downloaded to '" + filename + "'");
4022 h.showZoomButtons = true;
4023 h.showMenuButtons = true;
4024 h.forceRedraw = true;
4025 h.forceConvert = true;
4029 } else if (mode === "URL") {
4030 // Create new element
4031 let el = document.createElement('textarea');
4033 // Set value (string to be copied)
4034 let url = this.baseURL + "&group=" + this.group + "&panel=" + this.panel +
4035 "&A=" + this.tMin + "&B=" + this.tMax;
4036 url = encodeURI(url);
4039 // Set non-editable to avoid focus and move outside of view
4040 el.setAttribute('readonly', '');
4041 el.style = {position: 'absolute', left: '-9999px'};
4042 document.body.appendChild(el);
4043 // Select text inside element
4045 // Copy text to clipboard
4046 document.execCommand('copy');
4047 // Remove temporary element
4048 document.body.removeChild(el);
4050 dlgMessage("Info", "URL<br/><br/>" + url + "<br/><br/>copied to clipboard", true, false);