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.contentEditable === true) 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[0] >= this.data[index].time[this.data[index].time.length-1]) {
1314 // remove double event
1315 while (t1[0] === this.data[index].time[this.data[index].time.length-1]) {
1318 if (v1Raw.length > 0)
1319 v1Raw = v1Raw.slice(1);
1322 // add data to the right
1323 this.data[index].time = this.data[index].time.concat(t1);
1324 this.data[index].value = this.data[index].value.concat(v1);
1325 if (v1Raw.length > 0)
1326 this.data[index].rawValue = this.data[index].rawValue.concat(v1Raw);
1328 } else if (t1[t1.length-1] < this.data[index].time[0]) {
1330 // add data to the left
1331 this.data[index].time = t1.concat(this.data[index].time);
1332 this.data[index].value = v1.concat(this.data[index].value);
1333 if (v1Raw.length > 0)
1334 this.data[index].rawValue = v1Raw.concat(this.data[index].rawValue);
1338 for (let i = 1; i < this.data[index].time.length; i++)
1339 if (this.data[index].time[i] < this.data[index].time[i - 1]) {
1340 console.log("Error non-continuous data");
1341 log_hs_read("told", told[0], told[told.length-1]);
1342 log_hs_read("t1", t1[0], t1[t1.length-1]);
1351MhistoryGraph.prototype.receiveDataBinned = function (rpc) {
1353 // decode binary array
1354 let array = new Float64Array(rpc);
1356 // let status = array[0];
1357 // let startTime = array[1];
1358 // let endTime = array[2];
1359 let numBins = array[3];
1360 let nVars = array[4];
1363 // let hsStatus = array.slice(i, i + nVars);
1365 let numEntries = array.slice(i, i + nVars);
1367 // let lastTime = array.slice(i, i + nVars);
1369 // let lastValue = array.slice(i, i + nVars);
1372 if (i >= array.length) {
1373 // RPC did not return any data
1375 if (this.data === undefined) {
1376 // must initialize the arrays otherwise nothing works
1378 for (let index = 0; index < nVars; index++) {
1379 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1386 // push empty arrays on the first time
1387 if (this.data === undefined) {
1389 for (let index = 0; index < nVars; index++) {
1390 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1397 // create arrays of new values
1398 for (let index = 0; index < nVars; index++) {
1399 if (numEntries[index] === 0)
1406 // add data to the right
1407 let formula = this.param["Formula"];
1408 if (Array.isArray(formula))
1409 formula = formula[index];
1411 if (formula === undefined || formula === "") {
1412 for (let j = 0; j < numBins; j++) {
1414 let count = array[i++];
1415 // let mean = array[i++];
1416 // let rms = array[i++];
1418 let minValue = array[i++];
1419 let maxValue = array[i++];
1420 let firstTime = array[i++];
1421 let firstValue = array[i++];
1422 let lastTime = array[i++];
1423 let lastValue = array[i++];
1424 let t = Math.floor((firstTime + lastTime) / 2);
1427 // append to the right
1432 bin.firstValue = firstValue;
1433 bin.lastValue = lastValue;
1434 bin.minValue = minValue;
1435 bin.maxValue = maxValue;
1439 // calculate average bin count
1445 } else { // use formula
1447 for (let j = 0; j < numBins; j++) {
1449 let count = array[i++];
1450 // let mean = array[i++];
1451 // let rms = array[i++];
1453 let minValue = array[i++];
1454 let maxValue = array[i++];
1455 let firstTime = array[i++];
1456 let firstValue = array[i++];
1457 let lastTime = array[i++];
1458 let lastValue = array[i++];
1461 // append to the right
1462 t1.push(Math.floor((firstTime + lastTime) / 2));
1467 binRaw.firstValue = firstValue;
1468 bin.firstValue = eval(formula);
1470 binRaw.lastValue = lastValue;
1471 bin.lastValue = eval(formula);
1473 binRaw.minValue = minValue;
1474 bin.minValue = eval(formula);
1476 binRaw.maxValue = maxValue;
1477 bin.maxValue = eval(formula);
1480 binRaw1.push(binRaw);
1482 // calculate average bin count
1489 if (t1.length > 0) {
1491 let da = new Date(t1[0]*1000);
1492 let db = new Date(t1[t1.length-1]*1000);
1495 log_hs_read("receiveData binned", t1[0], t1[t1.length-1]);
1497 if (this.data[index].time.length === 0 ||
1498 t1[0] > this.data[index].time[0]) {
1500 // append to right if new data
1501 this.data[index].time = this.data[index].time.concat(t1);
1502 this.data[index].bin = this.data[index].bin.concat(bin1);
1503 if (binRaw1.length > 0)
1504 this.data[index].binRaw = this.data[index].rawValue.concat(binRaw1);
1508 // append to left if old data
1509 this.data[index].time = t1.concat(this.data[index].time);
1510 this.data[index].bin = bin1.concat(this.data[index].bin);
1511 if (binRaw1.length > 0)
1512 this.data[index].binRaw = binRaw1.concat(this.data[index].binRaw);
1517 // calculate average bin size
1519 this.binSize = binSize / binSizeN;
1524MhistoryGraph.prototype.loadNewData = function () {
1526 if (this.updateTimer)
1527 window.clearTimeout(this.updateTimer);
1529 // don't update window if content is hidden (other tab, minimized, etc.)
1530 if (document.hidden) {
1531 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1535 // don't update if not in scrolling mode
1537 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1541 // update data from last point to current time
1542 let t1 = this.tMaxReceived;
1543 if (t1 === undefined)
1545 let t2 = Math.floor(new Date() / 1000);
1547 // for strip-chart mode always use non-binned data
1548 this.binned = false;
1550 log_hs_read("loadNewData un-binned", t1, t2);
1551 mjsonrpc_call("hs_read_arraybuffer",
1553 "start_time": Math.floor(t1),
1554 "end_time": Math.floor(t2),
1555 "events": this.events,
1559 .then(function (rpc) {
1561 if (this.tMinRequested === undefined || t1 < this.tMinRequested) {
1562 this.tMinRequested = t1;
1563 this.tMinReceived = t1;
1566 if (this.tMaxRequested === undefined || t2 > this.tMaxRequested) {
1567 this.tMaxReceived = t2;
1568 this.tMaxRequested = t2;
1571 if (this.receiveData(rpc)) {
1573 this.scrollRedraw();
1576 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 1000);
1578 }.bind(this)).catch(function (error) {
1579 mjsonrpc_error_alert(error);
1583MhistoryGraph.prototype.scrollRedraw = function () {
1584 if (this.scrollTimer)
1585 window.clearTimeout(this.scrollTimer);
1588 let dt = this.tMax - this.tMin;
1589 this.tMax = new Date() / 1000;
1590 this.tMin = this.tMax - dt;
1594 // calculate time for one pixel
1595 dt = (this.tMax - this.tMin) / (this.x2 - this.x1);
1596 dt = Math.min(Math.max(0.1, dt), 60);
1597 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), dt / 2 * 1000);
1599 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
1604MhistoryGraph.prototype.discardCurrentData = function () {
1606 if (this.data === undefined)
1610 for (let i = 0 ; i<this.data.length ; i++) {
1611 this.data[i].time.length = 0;
1612 if (this.data[i].value)
1613 this.data[i].value.length = 0;
1614 if (this.data[i].bin)
1615 this.data[i].bin.length = 0;
1616 if (this.data[i].rawBin)
1617 this.data[i].rawBin.length = 0;
1618 if (this.data[i].rawValue)
1619 this.data[i].rawValue.length = 0;
1621 this.x[i].length = 0;
1622 this.y[i].length = 0;
1623 this.t[i].length = 0;
1624 this.v[i].length = 0;
1626 this.vRaw[i].length = 0;
1629 this.tMaxReceived = undefined ;
1630 this.tMaxRequested = undefined;
1631 this.tMinRequested = undefined;
1632 this.tMinReceived = undefined;
1635function binarySearch(array, target) {
1637 let endIndex = array.length - 1;
1639 while (startIndex <= endIndex) {
1640 middleIndex = Math.floor((startIndex + endIndex) / 2);
1641 if (target === array[middleIndex])
1644 if (target > array[middleIndex])
1645 startIndex = middleIndex + 1;
1646 if (target < array[middleIndex])
1647 endIndex = middleIndex - 1;
1653function splitEventAndTagName(var_name) {
1656 for (let i = 0; i < var_name.length; i++) {
1657 if (var_name[i] == ':') {
1662 let slash_pos = var_name.indexOf("/");
1663 let uses_per_variable_naming = (slash_pos != -1);
1665 if (uses_per_variable_naming && colons.length % 2 == 1) {
1666 let middle_colon_pos = colons[Math.floor(colons.length / 2)];
1667 let slash_to_mid = var_name.substr(slash_pos + 1, middle_colon_pos - slash_pos - 1);
1668 let mid_to_end = var_name.substr(middle_colon_pos + 1);
1670 if (slash_to_mid == mid_to_end) {
1671 // Special case - we have a string of the form Beamlime/GS2:FC1:GS2:FC1.
1672 // Logger has already warned people that having colons in the equipment/event
1673 // names is a bad idea, so we only need to worry about them in the tag name.
1674 split_pos = middle_colon_pos;
1676 // We have a string of the form Beamlime/Demand:GS2:FC1. Split at the first colon.
1677 split_pos = colons[0];
1680 // Normal case - split at the fist colon.
1681 split_pos = colons[0];
1684 let event_name = var_name.substr(0, split_pos);
1685 let tag_name = var_name.substr(split_pos + 1);
1687 return [event_name, tag_name];
1690MhistoryGraph.prototype.mouseEvent = function (e) {
1692 // fix buttons for IE
1693 if (!e.which && e.button) {
1694 if ((e.button & 1) > 0) e.which = 1; // Left
1695 else if ((e.button & 4) > 0) e.which = 2; // Middle
1696 else if ((e.button & 2) > 0) e.which = 3; // Right
1699 // extract X and Y coordinates
1702 if (e.type === "touchstart" || e.type === "touchmove") {
1703 eventX = e.touches[0].clientX;
1704 eventY = e.touches[0].clientY;
1705 let rect = e.target.getBoundingClientRect();
1706 eventX = eventX - Math.round(rect.left);
1707 eventY = eventY - Math.round(rect.top);
1708 } else if (e.type === "mousedown" || e.type === "mousemove" || e.type === "mouseup" ||
1709 e.type === "dblclick") {
1714 let cursor = this.pendingUpdates > 0 ? "progress" : "default";
1718 // cancel dragging in case we did not catch the mouseup event
1719 if (e.type === "mousemove" && e.buttons === 0 &&
1720 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
1723 if (e.type === "mousedown" || (e.type === "touchstart" && e.touches.length === 1)) {
1725 this.intSelector.style.display = "none";
1726 this.downloadSelector.style.display = "none";
1728 // check for buttons
1729 this.button.forEach(b => {
1730 if (eventX > b.x1 && eventX < b.x1 + b.width &&
1731 eventY > b.y1 && eventY < b.y1 + b.width &&
1737 // check for zoom buttons
1739 if (this.showMenuButtons)
1740 xb = this.width - 26 - 40;
1742 xb = this.width - 41;
1743 if (eventX > xb && eventX < xb + 20 &&
1744 eventY > this.y1 - 20 && eventY < this.y1) {
1746 let delta = this.tMax - this.tMin;
1748 this.tMin += delta / 2; // only zoom on left side in scroll mode
1750 this.tMin += delta / 4;
1751 this.tMax -= delta / 4; // zoom to center
1754 this.loadFullData(this.tMin, this.tMax);
1756 if (this.callbacks.timeZoom !== undefined)
1757 this.callbacks.timeZoom(this);
1762 if (eventX > xb + 20 && eventX < xb + 40 &&
1763 eventY > this.y1 - 20 && eventY < this.y1) {
1765 if (this.pendingUpdates > 0) {
1766 dlgMessage("Warning", "Don't press the '-' too fast!", true, false);
1768 let delta = this.tMax - this.tMin;
1769 this.tMin -= delta / 2;
1770 this.tMax += delta / 2;
1771 // don't go into the future
1772 let now = Math.floor(new Date() / 1000);
1773 if (this.tMax > now) {
1775 this.tMin = now - 2*delta;
1778 this.loadFullData(this.tMin, this.tMax);
1780 if (this.callbacks.timeZoom !== undefined)
1781 this.callbacks.timeZoom(this);
1784 if (e.type === "mousedown" ) {
1790 // check for dragging
1791 if (eventX > this.x1 && eventX < this.x2 &&
1792 eventY > this.y2 && eventY < this.y1) {
1793 this.drag.active = true;
1794 this.marker.active = false;
1795 this.scroll = false;
1796 this.drag.xStart = eventX;
1797 this.drag.yStart = eventY;
1798 this.drag.tStart = this.xToTime(eventX);
1799 this.drag.tMinStart = this.tMin;
1800 this.drag.tMaxStart = this.tMax;
1801 this.drag.yMinStart = this.yMin;
1802 this.drag.yMaxStart = this.yMax;
1803 this.drag.vStart = this.yToValue(eventY);
1806 // check for axis dragging
1807 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1) {
1808 this.zoom.x.active = true;
1809 this.scroll = false;
1810 this.zoom.x.x1 = eventX;
1811 this.zoom.x.x2 = undefined;
1812 this.zoom.x.t1 = this.xToTime(eventX);
1814 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1) {
1815 this.zoom.y.active = true;
1816 this.scroll = false;
1817 this.zoom.y.y1 = eventY;
1818 this.zoom.y.y2 = undefined;
1819 this.zoom.y.v1 = this.yToValue(eventY);
1824 if (cancel || e.type === "mouseup" || e.type === "touchend") {
1826 if (this.drag.active) {
1827 this.drag.active = false;
1830 if (this.zoom.x.active) {
1831 if (this.zoom.x.x2 !== undefined &&
1832 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
1833 let t1 = this.zoom.x.t1;
1834 let t2 = this.xToTime(this.zoom.x.x2);
1836 [t1, t2] = [t2, t1];
1842 this.zoom.x.active = false;
1844 this.loadFullData(this.tMin, this.tMax);
1846 if (this.callbacks.timeZoom !== undefined)
1847 this.callbacks.timeZoom(this);
1850 if (this.zoom.y.active) {
1851 if (this.zoom.y.y2 !== undefined &&
1852 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
1853 let v1 = this.zoom.y.v1;
1854 let v2 = this.yToValue(this.zoom.y.y2);
1856 [v1, v2] = [v2, v1];
1860 this.zoom.y.active = false;
1868 if (e.type === "touchstart" && e.touches.length === 2) {
1870 // start pinch / zoom
1872 let rect = e.target.getBoundingClientRect();
1874 this.zoom.x.pinch = true;
1875 this.zoom.x.x1 = e.touches[0].clientX - Math.round(rect.left);
1876 this.zoom.x.x2 = e.touches[1].clientX - Math.round(rect.left);
1877 this.zoom.x.t1 = this.xToTime(this.zoom.x.x1);
1878 this.zoom.x.t2 = this.xToTime(this.zoom.x.x2);
1880 this.zoom.y.pinch = true;
1881 this.zoom.y.y1 = e.touches[0].clientY - Math.round(rect.top);
1882 this.zoom.y.y2 = e.touches[1].clientY - Math.round(rect.top);
1883 this.zoom.y.v1 = this.yToValue(this.zoom.y.y1);
1884 this.zoom.y.v2 = this.yToValue(this.zoom.y.y2);
1886 let w = Math.abs(this.zoom.x.x2 - this.zoom.x.x1);
1887 let h = Math.abs(this.zoom.y.y2 - this.zoom.y.y1);
1890 this.zoom.x.pinch = false;
1892 this.zoom.y.pinch = false;
1894 if (this.zoom.y.pinch)
1899 if (e.type === "touchmove" && e.touches.length === 2) {
1903 let rect = e.target.getBoundingClientRect();
1904 let x1 = e.touches[0].clientX - Math.round(rect.left);
1905 let x2 = e.touches[1].clientX - Math.round(rect.left);
1906 let y1 = e.touches[0].clientY - Math.round(rect.top);
1907 let y2 = e.touches[1].clientY - Math.round(rect.top);
1909 // solution to linear equation:
1910 // xToTime(x1) =!= this.zoom.x.t1
1911 // xToTime(x2) =!= this.zoom.x.t2
1913 if (this.zoom.x.pinch) {
1914 let a = (x1 - this.x1) / (this.x2 - this.x1);
1915 let b = (x2 - this.x1) / (this.x2 - this.x1);
1917 let tMin = (a * this.zoom.x.t2 - b * this.zoom.x.t1) / (a - b);
1918 let tMax = ((a - 1) * this.zoom.x.t2 - (b - 1) * this.zoom.x.t1) / (a - b);
1920 if (tMax > tMin + 10) {
1925 this.loadSideData();
1928 if (this.zoom.y.pinch) {
1929 let a = (this.y1 - y1) / (this.y1 - this.y2);
1930 let b = (this.y1 - y2) / (this.y1 - this.y2);
1932 let yMin = (a * this.zoom.y.v2 - b * this.zoom.y.v1) / (a - b);
1933 let yMax = ((a - 1) * this.zoom.y.v2 - (b - 1) * this.zoom.y.v1) / (a - b);
1944 if (e.type === "touchend") {
1945 if (this.zoom.x.pinch)
1946 this.loadFullData(this.tMin, this.tMax);
1948 this.zoom.x.pinch = false;
1949 this.zoom.y.pinch = false;
1952 if (e.type === "mousemove" || ((e.type === "touchmove" || e.type === "touchstart") && e.touches.length === 1) ) {
1954 if (this.drag.active) {
1958 let dt = Math.round((eventX - this.drag.xStart) / (this.x2 - this.x1) * (this.tMax - this.tMin));
1959 this.tMin = this.drag.tMinStart - dt;
1960 this.tMax = this.drag.tMaxStart - dt;
1961 this.drag.lastDt = (eventX - this.drag.lastOffsetX) / (this.x2 - this.x1) * (this.tMax - this.tMin);
1962 this.drag.lastT = new Date().getTime();
1963 this.drag.lastOffsetX = eventX;
1969 let dy = eventY - this.drag.yStart;
1971 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));
1972 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));
1980 let dy = (this.drag.yStart - eventY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
1981 this.yMin = this.drag.yMinStart - dy;
1982 this.yMax = this.drag.yMaxStart - dy;
1987 this.loadSideData();
1991 if (this.callbacks.timeZoom !== undefined)
1992 this.callbacks.timeZoom(this);
1996 if (!this.drag.active || e.type === "touchstart" || e.type === "touchmove") {
2000 // change cursor to pointer over buttons
2001 this.button.forEach(b => {
2002 if (eventX > b.x1 && eventY > b.y1 &&
2003 eventX < b.x1 + b.width && eventY < b.y1 + b.height) {
2009 if (this.showZoomButtons) {
2012 if (this.showMenuButtons)
2013 xb = this.width - 26 - 40;
2015 xb = this.width - 41;
2017 // check for zoom buttons
2018 if (eventX > xb && eventX < xb + 20 &&
2019 eventY > this.y1 - 20 && eventY < this.y1) {
2023 if (eventX > xb + 20 && eventX < xb + 40 &&
2024 eventY > this.y1 - 20 && eventY < this.y1) {
2030 // display zoom cursor
2031 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1)
2032 cursor = "ew-resize";
2033 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1)
2034 cursor = "ns-resize";
2036 // execute axis zoom
2037 if (this.zoom.x.active) {
2038 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, eventX));
2039 this.zoom.x.t2 = this.xToTime(eventX);
2042 if (this.zoom.y.active) {
2043 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, eventY));
2044 this.zoom.y.v2 = this.yToValue(eventY);
2048 // check if cursor close to graph point
2049 if (this.data !== undefined && this.x.length && this.y.length) {
2051 let minDist = 10000;
2052 let markerX, markerY, markerT, markerV;
2053 for (let di = 0; di < this.data.length; di++) {
2055 if (this.solo.active && di !== this.solo.index)
2058 let i1 = binarySearch(this.x[di], eventX - 10);
2059 let i2 = binarySearch(this.x[di], eventX + 10);
2062 for (let i = i1; i <= i2; i++) {
2063 let d = (eventX - this.x[di][i]) * (eventX - this.x[di][i]) +
2064 (eventY - this.y[di][i]) * (eventY - this.y[di][i]);
2067 markerX = this.x[di][i];
2068 markerY = this.y[di][i];
2069 markerT = this.t[di][i];
2070 markerV = this.v[di][i];
2072 if (this.param["Show raw value"] !== undefined &&
2073 this.param["Show raw value"][di])
2074 markerV = this.vRaw[di][i];
2076 markerV = this.v[di][i];
2078 this.marker.graphIndex = di;
2079 this.marker.index = i;
2085 for (let i = i1; i <= i2; i++) {
2086 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2087 (eventY - this.p[di][i].max) * (eventY - this.p[di][i].max);
2090 markerX = this.p[di][i].x;
2091 markerY = this.p[di][i].max;
2092 markerT = this.p[di][i].t;
2094 if (this.param["Show raw value"] !== undefined &&
2095 this.param["Show raw value"][di])
2096 markerV = this.p[di][i].rawMaxValue;
2098 markerV = this.p[di][i].maxValue;
2100 this.marker.graphIndex = di;
2101 this.marker.index = i;
2106 for (let i = i1; i <= i2; i++) {
2107 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2108 (eventY - this.p[di][i].min) * (eventY - this.p[di][i].min);
2111 markerX = this.p[di][i].x;
2112 markerY = this.p[di][i].min;
2113 markerT = this.p[di][i].t;
2115 if (this.param["Show raw value"] !== undefined &&
2116 this.param["Show raw value"][di])
2117 markerV = this.p[di][i].rawMinValue;
2119 markerV = this.p[di][i].minValue;
2121 this.marker.graphIndex = di;
2122 this.marker.index = i;
2129 // exclude zoom buttons if visible
2130 let exclude = false;
2131 if (this.showZoomButtons &&
2132 eventX > this.width - 26 - 40 && this.offsetX < this.width - 26 &&
2133 eventY > this.y1 - 20 && eventY < this.y1) {
2136 // exclude label area
2137 if (this.showLabels &&
2138 eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7 &&
2139 eventY > this.y2 && eventY < this.y2 + this.variablesHeight + 2) {
2144 this.marker.active = false;
2146 this.marker.active = Math.sqrt(minDist) < 10 && eventX > this.x1 && eventX < this.x2;
2147 if (this.marker.active) {
2148 this.marker.x = markerX;
2149 this.marker.y = markerY;
2150 this.marker.t = markerT;
2151 this.marker.v = markerV;
2153 this.marker.mx = eventX;
2154 this.marker.my = eventY;
2157 if (this.marker.active)
2159 if (!this.marker.active && this.marker.activeOld)
2161 this.marker.activeOld = this.marker.active;
2170 if (e.type === "dblclick") {
2172 // check if inside zoom buttons
2173 if (eventX > this.width - 26 - 40 && eventX < this.width - 26 &&
2174 eventY > this.y1 - 20 && eventY < this.y1) {
2179 // measure distance to graphs
2180 if (this.data !== undefined && this.x.length && this.y.length) {
2182 // check if inside label area
2184 if (this.showLabels) {
2185 if (eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7) {
2186 let i = Math.floor((eventY - (this.y2 + 4)) / 17);
2187 if (i < this.data.length) {
2188 this.solo.active = true;
2189 this.solo.index = i;
2198 for (let di = 0; di < this.data.length; di++) {
2199 for (let i = 0; i < this.x[di].length; i++) {
2200 if (this.x[di][i] > this.x1 && this.x[di][i] < this.x2) {
2201 let d = Math.sqrt(Math.pow(eventX - this.x[di][i], 2) +
2202 Math.pow(eventY - this.y[di][i], 2));
2205 this.solo.index = di;
2210 // check if close to graph point
2211 if (minDist < 10 && eventX > this.x1 && eventX < this.x2)
2212 this.solo.active = !this.solo.active;
2221 this.parentDiv.title = title;
2222 this.parentDiv.style.cursor = cursor;
2227MhistoryGraph.prototype.mouseWheelEvent = function (e) {
2229 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2230 e.offsetY > this.y2 && e.offsetY < this.y1) {
2232 if (e.altKey || e.shiftKey) {
2236 let f = (e.offsetY - this.y1) / (this.y2 - this.y1);
2238 let step = e.deltaY / 100;
2244 let dtMin = f * (this.yMax - this.yMin) * step;
2245 let dtMax = (1 - f) * (this.yMax - this.yMin) * step;
2247 if (((this.yMax + dtMax) - (this.yMin - dtMin)) / (this.yMax0 - this.yMin0) < 1000 &&
2248 (this.yMax0 - this.yMin0) / ((this.yMax + dtMax) - (this.yMin - dtMin)) < 1000) {
2252 if (this.logAxis && this.yMin <= 0)
2254 if (this.logAxis && this.yMax <= 0)
2260 } else if (e.ctrlKey || e.metaKey) {
2262 this.showZoomButtons = false;
2265 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2266 let m = e.deltaY / 100;
2271 let dtMin = Math.abs(f * (this.tMax - this.tMin) * m);
2272 let dtMax = Math.abs((1 - f) * (this.tMax - this.tMin) * m);
2283 this.loadFullData(this.tMin, this.tMax);
2294 this.loadFullData(this.tMin, this.tMax);
2297 if (this.callbacks.timeZoom !== undefined)
2298 this.callbacks.timeZoom(this);
2300 } else if (e.deltaX !== 0) {
2302 let dt = (this.tMax - this.tMin) / 1000 * e.deltaX;
2307 this.loadSideData();
2313 this.marker.active = false;
2319MhistoryGraph.prototype.resetAxes = function () {
2320 this.tMax = Math.floor(new Date() / 1000);
2321 this.tMin = this.tMax - this.tScale;
2325 this.showZoomButtons = true;
2326 this.loadFullData(this.tMin, this.tMax);
2329MhistoryGraph.prototype.jumpToCurrent = function () {
2330 let dt = Math.floor(this.tMax - this.tMin);
2332 // limit to one week maximum (otherwise we have to read binned data)
2336 this.tMax = Math.floor(new Date() / 1000);
2337 this.tMin = this.tMax - dt;
2340 this.loadFullData(this.tMin, this.tMax);
2343MhistoryGraph.prototype.setTimespan = function (tMin, tMax, scroll) {
2346 this.scroll = scroll;
2348 this.loadFullData(tMin, tMax, scroll);
2351MhistoryGraph.prototype.resize = function () {
2352 this.canvas.width = this.parentDiv.clientWidth;
2353 this.canvas.height = this.parentDiv.clientHeight;
2354 this.width = this.parentDiv.clientWidth;
2355 this.height = this.parentDiv.clientHeight;
2357 if (this.intSelector !== undefined)
2358 this.intSelector.style.display = "none";
2360 this.forceConvert = true;
2364MhistoryGraph.prototype.redraw = function (force) {
2365 this.forceRedraw = force;
2366 let f = this.draw.bind(this);
2367 window.requestAnimationFrame(f);
2370MhistoryGraph.prototype.timeToXInit = function () {
2371 this.timeToXScale = 1 / (this.tMax - this.tMin) * (this.x2 - this.x1);
2374MhistoryGraph.prototype.timeToX = function (t) {
2375 return (t - this.tMin) * this.timeToXScale + this.x1;
2378MhistoryGraph.prototype.truncateInfinity = function(v) {
2379 if (v === Infinity) {
2380 return Number.MAX_VALUE;
2381 } else if (v === -Infinity) {
2382 return -Number.MAX_VALUE;
2388MhistoryGraph.prototype.valueToYInit = function () {
2389 // Avoid overflow of max - min > inf
2390 let max_scaled = this.yMax / 1e4;
2391 let min_scaled = this.yMin / 1e4;
2392 this.valueToYScale = (this.y1 - this.y2) * 1e-4 / (max_scaled - min_scaled);
2395MhistoryGraph.prototype.valueToY = function (v) {
2396 if (v === Infinity) {
2397 return this.yMax >= Number.MAX_VALUE ? this.y2 : 0;
2398 } else if (v === -Infinity) {
2399 return this.yMin <= -Number.MAX_VALUE ? this.y1 : this.y1 * 2;
2400 } else if (this.logAxis) {
2404 return this.y1 - (Math.log(v) - Math.log(this.yMin)) /
2405 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
2407 return this.y1 - (v - this.yMin) * this.valueToYScale;
2411MhistoryGraph.prototype.xToTime = function (x) {
2412 return (x - this.x1) / (this.x2 - this.x1) * (this.tMax - this.tMin) + this.tMin;
2415MhistoryGraph.prototype.yToValue = function (y) {
2416 if (!isFinite(this.yMax - this.yMin)) {
2417 // Contortions to avoid Infinity.
2418 let scaled = (this.yMax / 1e4) - (this.yMin / 1e4);
2419 let retval = ((((this.y1 - y) / (this.y1 - this.y2)) * scaled) + (this.yMin / 1e4)) * 1e4;
2423 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
2424 return Math.exp(yl);
2426 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
2429MhistoryGraph.prototype.findMinMax = function () {
2434 if (!this.autoscaleMin)
2435 this.yMin0 = this.param["Minimum"];
2437 if (!this.autoscaleMax)
2438 this.yMax0 = this.param["Maximum"];
2440 if (!this.autoscaleMin && !this.autoscaleMax) {
2441 this.yMin = this.yMin0;
2442 this.yMax = this.yMax0;
2446 let minValue = undefined;
2447 let maxValue = undefined;
2448 for (let index = 0; index < this.data.length; index++) {
2449 if (this.events[index] === "Run transitions")
2451 if (this.data[index].time.length === 0)
2453 if (this.solo.active && this.solo.index !== index)
2455 let i1 = binarySearch(this.data[index].time, this.tMin) + 1;
2456 let i2 = binarySearch(this.data[index].time, this.tMax);
2457 while ((minValue === undefined ||
2458 maxValue === undefined ||
2459 Number.isNaN(minValue) ||
2460 Number.isNaN(maxValue)) &&
2462 // find first valid value
2464 if (this.data[index].bin[i1].count !== 0) {
2465 minValue = this.data[index].bin[i1].minValue;
2466 maxValue = this.data[index].bin[i1].maxValue;
2469 minValue = this.data[index].value[i1];
2470 maxValue = this.data[index].value[i1];
2474 for (let i = i1; i <= i2; i++) {
2476 if (this.data[index].bin[i].count === 0)
2478 let v = this.data[index].bin[i].minValue;
2481 v = this.data[index].bin[i].maxValue;
2485 let v = this.data[index].value[i];
2486 if (Number.isNaN(v))
2496 // array could be empty (no data), so min/max would be NaN
2497 if (Number.isNaN(minValue) || Number.isNaN(maxValue))
2498 minValue = maxValue = 0;
2500 if (this.autoscaleMin)
2501 this.yMin0 = this.yMin = minValue;
2502 if (this.autoscaleMax)
2503 this.yMax0 = this.yMax = maxValue;
2505 if (minValue === undefined || maxValue === undefined) {
2510 if (this.yMin0 === this.yMax0) {
2515 if (this.yMax0 < this.yMin0)
2516 this.yMax0 = this.yMin0 + 1;
2519 if (this.autoscaleMin) {
2521 this.yMin = 0.8 * this.yMin0;
2523 // leave 10% space below graph
2524 this.yMin = this.yMin0 - this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2526 this.yMin = this.yMin0;
2527 if (this.logAxis && this.yMin <= 0)
2530 if (this.autoscaleMax) {
2532 this.yMax = 1.2 * this.yMax0;
2534 // leave 10% space above graph
2535 this.yMax = this.yMax0 + this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2537 this.yMax = this.yMax0;
2538 if (this.logAxis && this.yMax <= 0)
2542 this.yMax = this.truncateInfinity(this.yMax)
2543 this.yMin = this.truncateInfinity(this.yMin)
2546function convertLastWritten(last) {
2548 return "no data available";
2550 let d = new Date(last * 1000).toLocaleDateString(
2552 day: '2-digit', month: 'short', year: '2-digit',
2553 hour12: false, hour: '2-digit', minute: '2-digit'
2557 return "last data: " + d;
2560MhistoryGraph.prototype.updateURL = function() {
2561 let url = window.location.href;
2562 if (url.search("&A=") !== -1)
2563 url = url.slice(0, url.search("&A="));
2564 url += "&A=" + Math.round(this.tMin) + "&B=" + Math.round(this.tMax);
2566 if (url !== window.location.href)
2567 window.history.replaceState({}, "Midas History", url);
2570function createPinstripeCanvas() {
2571 const patternCanvas = document.createElement("canvas");
2572 const pctx = patternCanvas.getContext('2d', { antialias: true });
2573 const colour = "#FFC0C0";
2575 const CANVAS_SIDE_LENGTH = 90;
2576 const WIDTH = CANVAS_SIDE_LENGTH;
2577 const HEIGHT = CANVAS_SIDE_LENGTH;
2578 const DIVISIONS = 4;
2580 patternCanvas.width = WIDTH;
2581 patternCanvas.height = HEIGHT;
2582 pctx.fillStyle = colour;
2586 pctx.moveTo(0, HEIGHT * (1 / DIVISIONS));
2587 pctx.lineTo(WIDTH * (1 / DIVISIONS), 0);
2589 pctx.lineTo(0, HEIGHT * (1 / DIVISIONS));
2594 pctx.moveTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2595 pctx.lineTo(WIDTH * (1 / DIVISIONS), HEIGHT);
2596 pctx.lineTo(0, HEIGHT);
2597 pctx.lineTo(0, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2598 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), 0);
2599 pctx.lineTo(WIDTH, 0);
2600 pctx.lineTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2605 pctx.moveTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2606 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), HEIGHT);
2607 pctx.lineTo(WIDTH, HEIGHT);
2608 pctx.lineTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2611 return patternCanvas;
2614MhistoryGraph.prototype.draw = function () {
2617 // draw maximal 30 times per second
2618 if (!this.forceRedraw) {
2619 if (new Date().getTime() < this.lastDrawTime + 30)
2621 this.lastDrawTime = new Date().getTime();
2623 this.forceRedraw = false;
2625 let update_last_written = false;
2627 let ctx = this.canvas.getContext("2d");
2629 ctx.fillStyle = this.color.background;
2630 ctx.fillRect(0, 0, this.width, this.height);
2632 if (this.data === undefined) {
2634 ctx.font = "14px sans-serif";
2635 ctx.strokeStyle = "#808080";
2636 ctx.fillStyle = "#808080";
2637 ctx.textAlign = "center";
2638 ctx.textBaseline = "middle";
2639 ctx.fillText("Data being loaded ...", this.width / 2, this.height / 2);
2644 ctx.font = "14px sans-serif";
2646 if (this.height === undefined || this.width === undefined)
2648 if (this.yMin === undefined || Number.isNaN(this.yMin))
2650 if (this.yMax === undefined || Number.isNaN(this.yMax))
2653 let axisLabelWidth = this.drawVAxis(ctx, 50, this.height - 25, this.height - 35,
2654 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.logAxis, false);
2656 if (axisLabelWidth === undefined)
2659 if (this.showAxis) {
2660 this.x1 = axisLabelWidth + 15;
2661 this.y1 = this.height - 25;
2662 this.x2 = this.width - 26;
2666 this.y1 = this.height - 1;
2667 this.x2 = this.width - 26;
2671 if (this.showMenuButtons === false)
2672 this.x2 = this.width - 1;
2675 if (!this.floating && // suppress title since this is already in the dialog box
2678 ctx.strokeStyle = this.color.axis;
2679 ctx.fillStyle = "#F0F0F0";
2680 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, 20);
2681 ctx.fillRect(this.x1, 6, this.x2 - this.x1, 20);
2682 ctx.textAlign = "center";
2683 ctx.textBaseline = "middle";
2684 ctx.fillStyle = "#808080";
2686 if (this.group !== undefined)
2687 str += this.group + " - " + this.panel;
2688 else if (this.historyVar !== undefined)
2689 str += this.historyVar;
2691 if (this.debugString !== "")
2692 str += " - " + this.debugString;
2694 ctx.fillText(str, (this.x2 + this.x1) / 2, 16);
2697 let s = Math.round(this.binSize);
2698 ctx.textAlign = "right";
2699 ctx.fillText(s, this.x2 - 10, 16);
2702 // re-calculate axis scaling since x2, y2 might have been changed above
2703 this.timeToXInit(); // initialize scale factor t -> x
2704 this.valueToYInit(); // initialize scale factor v -> y
2707 ctx.strokeStyle = this.color.axis;
2708 ctx.drawLine(this.x1, this.y2, this.x2, this.y2);
2709 ctx.drawLine(this.x2, this.y2, this.x2, this.y1);
2711 if (this.logAxis && this.yMin < 1E-20)
2713 if (this.logAxis && this.yMax < 1E-18)
2715 this.drawVAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
2716 -4, -7, -10, -12, this.x2 - this.x1, this.yMin, this.yMax, this.logAxis, true);
2717 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
2718 4, 7, 10, 10, this.y2 - this.y1, this.tMin, this.tMax);
2720 // draw hatched area for "future"
2721 let t = new Date() / 1000;
2722 if (this.tMax > t) {
2723 let x = this.timeToX(t);
2726 ctx.fillStyle = ctx.createPattern(createPinstripeCanvas(), 'repeat');
2727 ctx.fillRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2729 ctx.strokeStyle = this.color.axis;
2730 ctx.strokeRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2733 // determine precision
2735 if (this.yMin === 0)
2738 n_sig1 = Math.floor(Math.log(Math.abs(this.yMin)) / Math.log(10)) -
2739 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2741 if (this.yMax === 0)
2744 n_sig2 = Math.floor(Math.log(Math.abs(this.yMax)) / Math.log(10)) -
2745 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2747 n_sig1 = Math.max(n_sig1, n_sig2);
2748 n_sig1 = Math.max(1, n_sig1);
2750 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2751 if (Math.abs(this.yMin) < 100000)
2752 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMin)) /
2753 Math.log(10) + 0.001) + 1);
2754 if (Math.abs(this.yMax) < 100000)
2755 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMax)) /
2756 Math.log(10) + 0.001) + 1);
2761 this.yPrecision = Math.max(6, n_sig1); // use at least 5 digits
2765 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2768 //profile("drawinit");
2771 for (let di = 0; di < this.data.length; di++)
2772 nPoints += this.data[di].time.length;
2774 // convert values to points if window has changed or number of points have changed
2775 if (this.tMin !== this.tMinOld || this.tMax !== this.tMaxOld ||
2776 this.yMin !== this.yMinOld || this.yMax !== this.yMaxOld ||
2777 nPoints !== this.nPointsOld || this.forceConvert) {
2779 this.tMinOld = this.tMin;
2780 this.tMaxOld = this.tMax;
2781 this.yMinOld = this.yMin;
2782 this.yMaxOld = this.yMax;
2783 this.nPointsOld = nPoints;
2784 this.forceConvert = false;
2787 for (let di = 0; di < this.data.length; di++) {
2789 if (this.x[di] === undefined) {
2790 this.x[di] = []; // x/y contain visible part of graph
2792 this.t[di] = []; // t/v contain time/value pairs corresponding to x/y
2795 this.vRaw[di] = []; // vRaw contains the value before the formula
2800 if (this.data[di].time.length === 0)
2803 let i1 = binarySearch(this.data[di].time, this.tMin);
2805 i1--; // add point to the left
2806 let i2 = binarySearch(this.data[di].time, this.tMax);
2807 if (i2 < this.data[di].time.length - 1)
2808 i2++; // add points to the right
2811 if (!this.binned || this.events[di] === "Run transitions") {
2812 for (let i = i1; i <= i2; i++) {
2813 let x = this.timeToX(this.data[di].time[i]);
2814 let y = this.valueToY(this.data[di].value[i]);
2819 if (!Number.isNaN(y)) {
2822 this.t[di][n] = this.data[di].time[i];
2823 this.v[di][n] = this.data[di].value[i];
2824 if (this.data[di].rawValue)
2825 this.vRaw[di][n] = this.data[di].rawValue[i];
2830 // truncate arrays if now shorter
2831 this.x[di].length = n;
2832 this.y[di].length = n;
2833 this.t[di].length = n;
2834 this.v[di].length = n;
2835 if (this.data[di].rawValue)
2836 this.vRaw[di].length = n;
2841 for (let i = i1; i <= i2; i++) {
2843 if (this.data[di].bin[i].count === 0)
2847 p.n = this.data[di].bin[i].count;
2848 p.x = Math.round(this.timeToX(this.data[di].time[i]));
2849 p.t = this.data[di].time[i];
2851 p.first = this.valueToY(this.data[di].bin[i].firstValue);
2852 p.min = this.valueToY(this.data[di].bin[i].minValue);
2853 p.minValue = this.data[di].bin[i].minValue;
2854 p.max = this.valueToY(this.data[di].bin[i].maxValue);
2855 p.maxValue = this.data[di].bin[i].maxValue;
2856 p.last = this.valueToY(this.data[di].bin[i].lastValue);
2858 if (this.data[di].binRaw) {
2859 p.rawFirstValue = this.data[di].binRaw[i].firstValue;
2860 p.rawMinValue = this.data[di].binRaw[i].minValue;
2861 p.rawMaxValue = this.data[di].binRaw[i].maxValue;
2862 p.rawLastValue = this.data[di].binRaw[i].lastValue;
2867 this.x[di][n] = p.x;
2872 // truncate arrays if now shorter
2873 this.p[di].length = n;
2874 this.x[di].length = n;
2875 if (this.data[di].rawValue)
2876 this.vRaw[di].length = n;
2881 // draw shaded areas
2882 if (this.showFill) {
2883 for (let di = 0; di < this.data.length; di++) {
2884 if (this.solo.active && this.solo.index !== di)
2887 if (this.events[di] === "Run transitions")
2890 ctx.fillStyle = this.param["Colour"][di];
2892 // don't draw lines over "gaps"
2893 let gap = this.timeToXScale * 600; // 10 min
2895 gap = 5; // minimum of 5 pixels
2898 if (this.p[di].length > 0) {
2899 let p = this.p[di][0];
2904 ctx.moveTo(p.x, p.first);
2905 ctx.lineTo(p.x, p.last);
2906 for (let i = 1; i < this.p[di].length; i++) {
2908 if (p.x - xold < gap) {
2909 ctx.lineTo(p.x, p.first);
2910 ctx.lineTo(p.x, p.last);
2912 ctx.lineTo(xold, this.valueToY(0));
2913 ctx.lineTo(p.x, this.valueToY(0));
2914 ctx.lineTo(p.x, p.first);
2915 ctx.lineTo(p.x, p.last);
2919 ctx.lineTo(xold, this.valueToY(0));
2920 ctx.lineTo(x0, this.valueToY(0));
2922 ctx.globalAlpha = 0.1;
2924 ctx.globalAlpha = 1;
2927 if (this.x[di].length > 0 && this.y[di].length > 0) {
2928 let x = this.x[di][0];
2929 let y = this.y[di][0];
2935 for (let i = 1; i < this.x[di].length; i++) {
2941 ctx.lineTo(xold, this.valueToY(0));
2942 ctx.lineTo(x, this.valueToY(0));
2947 ctx.lineTo(xold, this.valueToY(0));
2948 ctx.lineTo(x0, this.valueToY(0));
2950 ctx.globalAlpha = 0.1;
2952 ctx.globalAlpha = 1;
2958 // profile("Draw shaded areas");
2961 for (let di = 0; di < this.data.length; di++) {
2962 if (this.solo.active && this.solo.index !== di)
2965 if (this.events[di] === "Run transitions") {
2967 if (this.tags[di] === "State") {
2968 if (this.x[di].length < 200) {
2969 for (let i = 0; i < this.x[di].length; i++) {
2970 if (this.v[di][i] === 1) {
2971 ctx.strokeStyle = "#FF0000";
2972 ctx.fillStyle = "#808080";
2973 ctx.textAlign = "right";
2974 ctx.textBaseline = "top";
2975 ctx.fillText(this.v[di + 1][i], this.x[di][i] - 5, this.y2 + 3);
2976 } else if (this.v[di][i] === 3) {
2977 ctx.strokeStyle = "#00A000";
2978 ctx.fillStyle = "#808080";
2979 ctx.textAlign = "left";
2980 ctx.textBaseline = "top";
2981 ctx.fillText(this.v[di + 1][i], this.x[di][i] + 3, this.y2 + 3);
2983 ctx.strokeStyle = "#F9A600";
2986 ctx.setLineDash([8, 2]);
2987 ctx.drawLine(Math.floor(this.x[di][i]), this.y1, Math.floor(this.x[di][i]), this.y2);
2988 ctx.setLineDash([]);
2995 ctx.strokeStyle = this.param["Colour"][di];
2997 // don't draw lines over "gaps"
2998 let gap = this.timeToXScale * 600; // 10 min
3000 gap = 5; // minimum of 5 pixels
3003 if (this.p[di].length > 0) {
3004 let p = this.p[di][0];
3005 //console.log("di:" + di + " i:" + 0 + " x:" + p.x, " y:" + p.first);
3008 ctx.moveTo(p.x, p.first);
3009 ctx.lineTo(p.x, p.max + 1); // in case min==max
3010 ctx.lineTo(p.x, p.min);
3011 ctx.lineTo(p.x, p.last);
3012 for (let i = 1; i < this.p[di].length; i++) {
3014 //console.log("di:" + di + " i:" + i + " x:" + p.x, " y:" + p.first);
3015 if (p.x - xold < gap) {
3016 // draw lines first - max - min - last
3017 ctx.lineTo(p.x, p.first);
3018 ctx.lineTo(p.x, p.max + 1); // in case min==max
3019 ctx.lineTo(p.x, p.min);
3020 ctx.lineTo(p.x, p.last);
3021 } else { // don't draw gap
3022 // draw lines first - max - min - last
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);
3033 if (this.x[di].length === 1) {
3034 let x = this.x[di][0];
3035 let y = this.y[di][0];
3036 ctx.fillStyle = this.param["Colour"][di];
3037 ctx.fillRect(x - 1, y - 1, 3, 3);
3039 if (this.x[di].length > 0) {
3041 let x = this.x[di][0];
3042 let y = this.y[di][0];
3045 for (let i = 1; i < this.x[di].length; i++) {
3046 let x = this.x[di][i];
3047 let y = this.y[di][i];
3061 ctx.restore(); // remove clipping
3063 // profile("Draw graphs");
3065 // labels with variable names and values
3066 if (this.showLabels) {
3067 if (this.solo.active)
3068 this.variablesHeight = 17 + 7;
3070 this.variablesHeight = this.param["Variables"].length * 17 + 7;
3071 this.variablesWidth = 0;
3073 // determine width of widest label
3074 this.param["Variables"].forEach((v, i) => {
3076 if (this.param.Label[i] !== "") {
3077 width = ctx.measureText(this.param.Label[i]).width;
3079 width = ctx.measureText(splitEventAndTagName(v)[1]).width;
3082 if (this.param["Show raw value"] !== undefined &&
3083 this.param["Show raw value"][i])
3084 width += ctx.measureText(" (Raw)").width;
3086 width += 20; // space between name and value
3088 if (this.v[i] !== undefined && this.v[i].length > 0) {
3089 // use last point in array
3090 let index = this.v[i].length - 1;
3092 // use point at current marker
3093 if (this.marker.active)
3094 index = this.marker.index;
3096 if (index < this.v[i].length) {
3098 if (this.param["Show raw value"] !== undefined &&
3099 this.param["Show raw value"][i])
3100 value = this.vRaw[i][index];
3102 value = this.v[i][index];
3104 // convert value to string with 6 digits
3105 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3106 width += ctx.measureText(str).width;
3108 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3109 // use last point in array
3110 let index = this.p[i].length - 1;
3112 // use point at current marker
3113 if (this.marker.active)
3114 index = this.marker.index;
3116 if (index < this.p[i].length) {
3118 if (this.param["Show raw value"] !== undefined &&
3119 this.param["Show raw value"][i])
3120 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3122 value = (this.p[i][index].minValue + this.p[i][index].maxValue)/2;
3124 // convert value to string with 6 digits
3125 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3126 width += ctx.measureText(str).width;
3129 width += ctx.measureText(convertLastWritten(this.lastWritten[i])).width;
3132 this.variablesWidth = Math.max(this.variablesWidth, width);
3135 let xLabel = this.x1;
3136 if (this.solo.active)
3137 xLabel = this.x1 + 28;
3141 ctx.rect(xLabel, this.y2, 25 + this.variablesWidth + 7, this.variablesHeight + 2);
3144 ctx.strokeStyle = this.color.axis;
3145 ctx.fillStyle = "#F0F0F0";
3146 ctx.globalAlpha = 0.5;
3147 ctx.strokeRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3148 ctx.fillRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3149 ctx.globalAlpha = 1;
3151 this.param["Variables"].forEach((v, i) => {
3153 if (this.solo.active && i !== this.solo.index)
3157 if (!this.solo.active)
3161 ctx.strokeStyle = this.param["Colour"][i];
3162 ctx.drawLine(xLabel + 5, this.y2 + 14 + yLabel, xLabel + 20, this.y2 + 14 + yLabel);
3165 ctx.textAlign = "left";
3166 ctx.textBaseline = "middle";
3167 ctx.fillStyle = "#404040";
3170 if (this.param.Label[i] !== "")
3171 str = this.param.Label[i];
3173 str = splitEventAndTagName(v)[1];
3175 if (this.param["Show raw value"] !== undefined &&
3176 this.param["Show raw value"][i])
3179 ctx.fillText(str, xLabel + 25, this.y2 + 14 + yLabel);
3181 ctx.textAlign = "right";
3184 if (this.v[i] !== undefined && this.v[i].length > 0) {
3185 // use last point in array
3186 let index = this.v[i].length - 1;
3188 // use point at current marker
3189 if (this.marker.active)
3190 index = this.marker.index;
3192 if (index < this.v[i].length) {
3193 // convert value to string with 6 digits
3195 if (this.param["Show raw value"] !== undefined &&
3196 this.param["Show raw value"][i])
3197 value = this.vRaw[i][index];
3199 value = this.v[i][index];
3200 let str = value.toPrecision(this.yPrecision).stripZeros();
3201 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3203 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3207 // use last point in array
3208 let index = this.p[i].length - 1;
3210 // use point at current marker
3211 if (this.marker.active)
3212 index = this.marker.index;
3214 if (index < this.p[i].length) {
3215 // convert value to string with 6 digits
3217 if (this.param["Show raw value"] !== undefined &&
3218 this.param["Show raw value"][i])
3219 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3221 value = (this.p[i][index].minValue + this.p[i][index].maxValue) / 2;
3222 let str = value.toPrecision(this.yPrecision).stripZeros();
3223 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3228 if (this.lastWritten.length > 0) {
3229 if (this.lastWritten[i] > this.tMax) {
3230 //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));
3231 update_last_written = true;
3233 ctx.fillText(convertLastWritten(this.lastWritten[i]),
3234 xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3236 //console.log("last_written was not loaded yet");
3237 update_last_written = true;
3243 ctx.restore(); // remove clipping
3246 // "updating" notice
3247 if (this.pendingUpdates > 0 && this.tMinReceived > this.tMin) {
3248 let str = "Updating data ...";
3249 ctx.strokeStyle = "#404040";
3250 ctx.fillStyle = "#FFC0C0";
3251 ctx.fillRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3252 ctx.strokeRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3253 ctx.fillStyle = "#404040";
3254 ctx.textAlign = "left";
3255 ctx.textBaseline = "middle";
3256 ctx.fillText(str, this.x1 + 10, this.y1 - 13);
3261 for (let i = 0; i < this.data.length; i++) {
3262 if (this.data[i].time === undefined || this.data[i].time.length === 0) {
3268 // "empty window" notice
3270 ctx.font = "16px sans-serif";
3271 let str = "No data available";
3272 ctx.strokeStyle = "#404040";
3273 ctx.fillStyle = "#F0F0F0";
3274 let w = ctx.measureText(str).width + 10;
3276 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3277 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3278 ctx.fillStyle = "#404040";
3279 ctx.textAlign = "center";
3280 ctx.textBaseline = "middle";
3281 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
3282 ctx.font = "14px sans-serif";
3286 if (this.showMenuButtons) {
3288 let buttonSize = 20;
3289 this.button.forEach(b => {
3290 b.x1 = this.width - buttonSize - 6;
3291 b.y1 = 6 + y * (buttonSize + 4);
3292 b.width = buttonSize + 4;
3293 b.height = buttonSize + 4;
3296 if (b.src === "maximize-2.svg") {
3297 let s = window.location.href;
3298 if (s.indexOf("&A") > -1)
3299 s = s.substr(0, s.indexOf("&A"));
3300 if (s === encodeURI(this.baseURL + "&group=" + this.group + "&panel=" + this.panel)) {
3306 if (b.src === "corner-down-left.svg") {
3309 if (this.solo.active)
3317 if (b.src === "play.svg" && !this.scroll)
3318 ctx.fillStyle = "#FFC0C0";
3320 ctx.fillStyle = "#F0F0F0";
3321 ctx.strokeStyle = "#808080";
3322 ctx.fillRect(b.x1, b.y1, b.width, b.height);
3323 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
3324 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
3331 if (this.showZoomButtons) {
3333 if (this.showMenuButtons)
3334 xb = this.width - 26 - 40;
3336 xb = this.width - 41;
3337 let yb = this.y1 - 20;
3338 ctx.fillStyle = "#F0F0F0";
3339 ctx.globalAlpha = 0.5;
3340 ctx.fillRect(xb, yb, 20, 20);
3341 ctx.globalAlpha = 1;
3342 ctx.strokeStyle = "#808080";
3343 ctx.strokeRect(xb, yb, 20, 20);
3344 ctx.strokeStyle = "#202020";
3345 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3346 ctx.drawLine(xb + 10, yb + 4, xb + 10, yb + 17);
3349 ctx.globalAlpha = 0.5;
3350 ctx.fillRect(xb, yb, 20, 20);
3351 ctx.globalAlpha = 1;
3352 ctx.strokeStyle = "#808080";
3353 ctx.strokeRect(xb, yb, 20, 20);
3354 ctx.strokeStyle = "#202020";
3355 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3359 if (this.zoom.x.active) {
3360 ctx.fillStyle = "#808080";
3361 ctx.globalAlpha = 0.2;
3362 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
3363 ctx.globalAlpha = 1;
3364 ctx.strokeStyle = "#808080";
3365 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
3366 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
3368 if (this.zoom.y.active) {
3369 ctx.fillStyle = "#808080";
3370 ctx.globalAlpha = 0.2;
3371 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
3372 ctx.globalAlpha = 1;
3373 ctx.strokeStyle = "#808080";
3374 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
3375 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
3379 if (this.marker.active) {
3383 ctx.globalAlpha = 0.1;
3384 ctx.arc(this.marker.x, this.marker.y, 10, 0, 2 * Math.PI);
3385 ctx.fillStyle = "#000000";
3387 ctx.globalAlpha = 1;
3390 ctx.arc(this.marker.x, this.marker.y, 4, 0, 2 * Math.PI);
3391 ctx.fillStyle = "#000000";
3394 ctx.strokeStyle = "#A0A0A0";
3395 ctx.drawLine(this.marker.x, this.y1, this.marker.x, this.y2);
3398 let v = this.marker.v;
3401 if (this.param.Label[this.marker.graphIndex] !== "")
3402 s = this.param.Label[this.marker.graphIndex];
3404 s = this.param["Variables"][this.marker.graphIndex];
3406 if (this.param["Show raw value"] !== undefined &&
3407 this.param["Show raw value"][this.marker.graphIndex])
3410 s += ": " + v.toPrecision(this.yPrecision).stripZeros();
3412 let w = ctx.measureText(s).width + 6;
3413 let h = ctx.measureText("M").width * 1.2 + 6;
3414 let x = this.marker.mx + 20;
3415 let y = this.marker.my + h / 3 * 2;
3419 if (x + w >= this.x2) {
3420 x = this.marker.x - 20 - w;
3424 if (y > (this.y1 - this.y2) / 2) {
3425 y = this.marker.y - h / 3 * 5;
3429 ctx.strokeStyle = "#808080";
3430 ctx.fillStyle = "#F0F0F0";
3431 ctx.textBaseline = "middle";
3432 ctx.fillRect(x, y, w, h);
3433 ctx.strokeRect(x, y, w, h);
3434 ctx.fillStyle = "#404040";
3435 ctx.fillText(s, x + 3, y + h / 2);
3438 ctx.strokeStyle = "#808080";
3439 ctx.drawLine(this.marker.x, this.marker.y, xl, yl);
3442 s = timeToLabel(this.marker.t, 1, true);
3443 w = ctx.measureText(s).width + 10;
3444 h = ctx.measureText("M").width * 1.2 + 11;
3445 x = this.marker.x - w / 2;
3449 if (x + w >= this.x2)
3452 ctx.strokeStyle = "#808080";
3453 ctx.fillStyle = "#F0F0F0";
3454 ctx.fillRect(x, y, w, h);
3455 ctx.strokeRect(x, y, w, h);
3456 ctx.fillStyle = "#404040";
3457 ctx.fillText(s, x + 5, y + h / 2);
3460 this.lastDrawTime = new Date().getTime();
3462 // profile("Finished draw");
3464 if (update_last_written) {
3465 this.updateLastWritten();
3469 if (this.updateURLTimer !== undefined)
3470 window.clearTimeout(this.updateURLTimer);
3472 if (this.plotIndex === 0 && this.floating !== true)
3473 this.updateURLTimer = window.setTimeout(this.updateURL.bind(this), 10);
3476MhistoryGraph.prototype.drawVAxis = function (ctx, x1, y1, height, minor, major,
3477 text, label, grid, ymin, ymax, logaxis, draw) {
3478 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
3479 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
3480 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
3483 ctx.textAlign = "right";
3485 ctx.textAlign = "left";
3486 ctx.textBaseline = "middle";
3487 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
3489 if (ymax <= ymin || height <= 0)
3492 if (!isFinite(ymax - ymin) || ymax == Number.MAX_VALUE) {
3493 dy = Number.MAX_VALUE / 10;
3497 } else if (logaxis) {
3498 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
3507 // use 6 as min tick distance
3508 dy = (ymax - ymin) / (height / 6);
3510 int_dy = Math.floor(Math.log(dy) / Math.log(10));
3511 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
3518 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
3519 major_base = label_base = tick_base + 1;
3521 // rounding up of dy, label_dy
3522 dy = Math.pow(10, int_dy) * base[tick_base];
3523 major_dy = Math.pow(10, int_dy) * base[major_base];
3524 label_dy = major_dy;
3526 // number of significant digits
3530 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
3531 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3536 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
3537 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3539 n_sig1 = Math.max(n_sig1, n_sig2);
3540 n_sig1 = Math.max(1, n_sig1);
3542 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
3543 if (Math.abs(ymin) < 100000)
3544 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
3545 Math.log(10) + 0.001) + 1);
3546 if (Math.abs(ymax) < 100000)
3547 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
3548 Math.log(10) + 0.001) + 1);
3550 // increase label_dy if labels would overlap
3551 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
3553 label_dy = Math.pow(10, int_dy) * base[label_base];
3554 if (label_base % 3 === 2 && major_base % 3 === 1) {
3556 major_dy = Math.pow(10, int_dy) * base[major_base];
3561 y_act = Math.floor(ymin / dy) * dy;
3563 let last_label_y = y1;
3567 ctx.strokeStyle = this.color.axis;
3568 ctx.drawLine(x1, y1, x1, y1 - height);
3573 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
3574 (Math.log(ymax) - Math.log(ymin)) * height;
3575 else if (!(isFinite(ymax - ymin)))
3576 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
3578 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
3579 ys = Math.round(y_screen);
3581 if (y_screen < y1 - height - 0.001 || isNaN(ys))
3584 if (y_screen <= y1 + 0.001) {
3585 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
3586 dy / major_dy / 10.0) {
3588 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
3589 dy / label_dy / 10.0) {
3592 ctx.strokeStyle = this.color.axis;
3593 ctx.drawLine(x1, ys, x1 + text, ys);
3597 if (grid !== 0 && ys < y1 && ys > y1 - height)
3599 ctx.strokeStyle = this.color.grid;
3600 ctx.drawLine(x1, ys, x1 + grid, ys);
3606 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3607 str = y_act.toExponential(n_sig1).stripZeros();
3609 str = y_act.toPrecision(n_sig1).stripZeros();
3610 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3612 ctx.strokeStyle = this.color.label;
3613 ctx.fillStyle = this.color.label;
3614 ctx.fillText(str, x1 + label, ys);
3616 last_label_y = ys - textHeight / 2;
3621 ctx.strokeStyle = this.color.axis;
3622 ctx.drawLine(x1, ys, x1 + major, ys);
3626 if (grid !== 0 && ys < y1 && ys > y1 - height)
3628 ctx.strokeStyle = this.color.grid;
3629 ctx.drawLine(x1, ys, x1 + grid, ys);
3642 ctx.strokeStyle = this.color.axis;
3643 ctx.drawLine(x1, ys, x1 + minor, ys);
3646 // for logaxis, also put labels on minor tick marks
3650 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3651 str = y_act.toExponential(n_sig1).stripZeros();
3653 str = y_act.toPrecision(n_sig1).stripZeros();
3654 if (ys - textHeight / 2 > y1 - height &&
3655 ys + textHeight / 2 < y1 &&
3656 ys + textHeight < last_label_y + 2) {
3657 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3659 ctx.strokeStyle = this.color.label;
3660 ctx.fillStyle = this.color.label;
3661 ctx.fillText(str, x1 + label, ys);
3672 // suppress 1.23E-17 ...
3673 if (Math.abs(y_act) < dy / 100)
3683 day: '2-digit', month: 'short', year: '2-digit',
3684 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3689 day: '2-digit', month: 'short', year: '2-digit',
3690 hour12: false, hour: '2-digit', minute: '2-digit'
3695 day: '2-digit', month: 'short', year: '2-digit',
3696 hour12: false, hour: '2-digit', minute: '2-digit'
3701 day: '2-digit', month: 'short', year: '2-digit'
3706 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3711 hour12: false, hour: '2-digit', minute: '2-digit'
3716 hour12: false, hour: '2-digit', minute: '2-digit'
3721 day: '2-digit', month: 'short', year: '2-digit',
3722 hour12: false, hour: '2-digit', minute: '2-digit'
3727 day: '2-digit', month: 'short', year: '2-digit'
3730function timeToLabel(sec, base, forceDate) {
3731 let d = mhttpd_get_display_time(sec).date;
3735 return d.toLocaleTimeString('en-GB', options1);
3736 } else if (base < 600) {
3737 return d.toLocaleTimeString('en-GB', options2);
3738 } else if (base < 3600 * 24) {
3739 return d.toLocaleTimeString('en-GB', options3);
3741 return d.toLocaleDateString('en-GB', options4);
3746 return d.toLocaleTimeString('en-GB', options5);
3747 } else if (base < 600) {
3748 return d.toLocaleTimeString('en-GB', options6);
3749 } else if (base < 3600 * 3) {
3750 return d.toLocaleTimeString('en-GB', options7);
3751 } else if (base < 3600 * 24) {
3752 return d.toLocaleTimeString('en-GB', options8);
3754 return d.toLocaleDateString('en-GB', options9);
3758MhistoryGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
3759 text, label, grid, xmin, xmax) {
3760 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
3761 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
3763 ctx.textAlign = "left";
3764 ctx.textBaseline = "top";
3766 if (xmax <= xmin || width <= 0)
3769 /* force date display if xmax not today */
3770 let d1 = new Date(xmax * 1000);
3771 let d2 = new Date();
3772 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
3774 /* use 5 pixel as min tick distance */
3775 let dx = Math.round((xmax - xmin) / (width / 5));
3778 for (tick_base = 0; base[tick_base]; tick_base++) {
3779 if (base[tick_base] > dx)
3782 if (!base[tick_base])
3784 dx = base[tick_base];
3786 let major_base = tick_base;
3789 let label_base = major_base;
3793 let str = timeToLabel(xmin, label_dx, forceDate);
3794 let maxwidth = ctx.measureText(str).width;
3796 /* increasing label_dx, if labels would overlap */
3797 if (maxwidth > 0.75 * label_dx / (xmax - xmin) * width) {
3798 if (base[label_base + 1])
3799 label_dx = base[++label_base];
3801 label_dx += 3600 * 24;
3803 if (label_base > major_base + 1 || !base[label_base + 1]) {
3804 if (base[major_base + 1])
3805 major_dx = base[++major_base];
3807 major_dx += 3600 * 24;
3810 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3811 if (base[tick_base + 1])
3812 dx = base[++tick_base];
3821 let d = new Date(xmin * 1000);
3822 let tz = d.getTimezoneOffset() * 60;
3824 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3826 ctx.strokeStyle = this.color.axis;
3827 ctx.drawLine(x1, y1, x1 + width, y1);
3830 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3832 if (xs > x1 + width + 0.001)
3836 if ((x_act - tz) % major_dx === 0) {
3837 if ((x_act - tz) % label_dx === 0) {
3839 ctx.strokeStyle = this.color.axis;
3840 ctx.drawLine(xs, y1, xs, y1 + text);
3843 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3844 ctx.strokeStyle = this.color.grid;
3845 ctx.drawLine(xs, y1, xs, y1 + grid);
3850 let str = timeToLabel(x_act, label_dx, forceDate);
3852 // if labels at edge, shift them in
3853 let xl = xs - ctx.measureText(str).width / 2;
3856 if (xl + ctx.measureText(str).width >= xr)
3857 xl = xr - ctx.measureText(str).width - 1;
3858 ctx.strokeStyle = this.color.label;
3859 ctx.fillStyle = this.color.label;
3860 ctx.fillText(str, xl, y1 + label);
3864 ctx.strokeStyle = this.color.axis;
3865 ctx.drawLine(xs, y1, xs, y1 + major);
3869 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3870 ctx.strokeStyle = this.color.grid;
3871 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
3875 ctx.strokeStyle = this.color.axis;
3876 ctx.drawLine(xs, y1, xs, y1 + minor);
3885MhistoryGraph.prototype.download = function (mode) {
3887 let leftDate = mhttpd_get_display_time(this.tMin).date;
3888 let rightDate = mhttpd_get_display_time(this.tMax).date;
3889 let filename = this.group + "-" + this.panel + "-" +
3890 leftDate.getFullYear() +
3891 ("0" + (leftDate.getUTCMonth() + 1)).slice(-2) +
3892 ("0" + leftDate.getUTCDate()).slice(-2) + "-" +
3893 ("0" + leftDate.getUTCHours()).slice(-2) +
3894 ("0" + leftDate.getUTCMinutes()).slice(-2) +
3895 ("0" + leftDate.getUTCSeconds()).slice(-2) + "-" +
3896 rightDate.getFullYear() +
3897 ("0" + (rightDate.getUTCMonth() + 1)).slice(-2) +
3898 ("0" + rightDate.getUTCDate()).slice(-2) + "-" +
3899 ("0" + rightDate.getUTCHours()).slice(-2) +
3900 ("0" + rightDate.getUTCMinutes()).slice(-2) +
3901 ("0" + rightDate.getUTCSeconds()).slice(-2);
3903 // use trick from FileSaver.js
3904 let a = document.getElementById('downloadHook');
3906 a = document.createElement("a");
3907 a.style.display = "none";
3908 a.id = "downloadHook";
3909 document.body.appendChild(a);
3912 if (mode === "CSV") {
3916 this.param["Variables"].forEach(v => {
3919 data += v + " MIN," + v + " MAX,";
3923 data = data.slice(0, -1);
3927 let nvar = this.param["Variables"].length;
3928 for (let index=0 ; index < nvar ; index++)
3929 if (this.data[index].time.length > maxlen)
3930 maxlen = this.data[index].time.length;
3932 for (let di=0 ; di < nvar ; di++)
3933 for (let i = 0; i < maxlen; i++) {
3934 if (i < this.data[di].time.length &&
3935 this.data[di].time[i] > this.tMin) {
3941 for (let i = 0; i < maxlen; i++) {
3943 for (let di = 0 ; di < nvar ; di++) {
3944 if (index[di] < this.data[di].time.length &&
3945 this.data[di].time[index[di]] > this.tMin && this.data[di].time[index[di]] < this.tMax) {
3947 l += this.data[di].time[index[di]] + ",";
3949 if (this.param["Show raw value"] !== undefined &&
3950 this.param["Show raw value"][di]) {
3951 l += this.data[di].binRaw[index[di]].minValue + ",";
3952 l += this.data[di].binRaw[index[di]].maxValue + ",";
3954 l += this.data[di].bin[index[di]].minValue + ",";
3955 l += this.data[di].bin[index[di]].maxValue + ",";
3960 l += this.data[di].time[index[di]] + ",";
3962 if (this.param["Show raw value"] !== undefined &&
3963 this.param["Show raw value"][di])
3964 l += this.data[di].rawValue[index[di]] + ",";
3966 l += this.data[di].value[index[di]] + ",";
3973 if (l.split(',').some(s => s)) { // don't add if only commas
3974 l = l.slice(0, -1); // remove last comma
3979 let blob = new Blob([data], {type: "text/csv"});
3980 let url = window.URL.createObjectURL(blob);
3983 a.download = filename;
3985 window.URL.revokeObjectURL(url);
3986 dlgAlert("Data downloaded to '" + filename + "'");
3988 } else if (mode === "PNG") {
3991 this.showZoomButtons = false;
3992 this.showMenuButtons = false;
3993 this.forceRedraw = true;
3994 this.forceConvert = true;
3998 this.canvas.toBlob(function (blob) {
3999 let url = window.URL.createObjectURL(blob);
4002 a.download = filename;
4004 window.URL.revokeObjectURL(url);
4005 dlgAlert("Image downloaded to '" + filename + "'");
4007 h.showZoomButtons = true;
4008 h.showMenuButtons = true;
4009 h.forceRedraw = true;
4010 h.forceConvert = true;
4014 } else if (mode === "URL") {
4015 // Create new element
4016 let el = document.createElement('textarea');
4018 // Set value (string to be copied)
4019 let url = this.baseURL + "&group=" + this.group + "&panel=" + this.panel +
4020 "&A=" + this.tMin + "&B=" + this.tMax;
4021 url = encodeURI(url);
4024 // Set non-editable to avoid focus and move outside of view
4025 el.setAttribute('readonly', '');
4026 el.style = {position: 'absolute', left: '-9999px'};
4027 document.body.appendChild(el);
4028 // Select text inside element
4030 // Copy text to clipboard
4031 document.execCommand('copy');
4032 // Remove temporary element
4033 document.body.removeChild(el);
4035 dlgMessage("Info", "URL<br/><br/>" + url + "<br/><br/>copied to clipboard", true, false);