MIDAS
Loading...
Searching...
No Matches
mhistory.js
Go to the documentation of this file.
1//
2// Name: mhistory.js
3// Created by: Stefan Ritt
4//
5// Contents: JavaScript history plotting routines
6//
7// Note: please load midas.js, mhttpd.js and control.js before mhistory.js
8//
9
10LN10 = 2.302585094;
11LOG2 = 0.301029996;
12LOG5 = 0.698970005;
13
14function log_hs_read(str, a, b)
15{
16 let da = new Date(a*1000);
17 let db = new Date(b*1000);
18 let s = str + ": " +
19 da.toLocaleDateString() +
20 " " +
21 da.toLocaleTimeString() +
22 " ---- " +
23 db.toLocaleDateString() +
24 " " +
25 db.toLocaleTimeString();
26
27 // out-comment following line to log all history requests
28 //console.log(s);
29}
30
31function profile(flag) {
32 if (flag === true || flag === undefined) {
33 console.log("");
34 profile.startTime = new Date().getTime();
35 return;
36 }
37
38 let now = new Date().getTime();
39 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
40 profile.startTime = new Date().getTime();
41}
42
43function mhistory_init(mhist, noBorder, param) {
44 // go through all data-name="mhistory" tags if not passed
45 let floating;
46 if (mhist === undefined) {
47 var mhist = document.getElementsByClassName("mjshistory");
48 } else if (!Array.isArray(mhist)) {
49 mhist = [mhist];
50 floating = true;
51 }
52
53 let baseURL = window.location.href;
54 if (baseURL.indexOf("?cmd") > 0)
55 baseURL = baseURL.substring(0, baseURL.indexOf("?cmd"));
56 baseURL += "?cmd=history";
57
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 () {
65 this.mhg.resize();
66 };
67 }
68}
69
70function mhistory_dialog_var(historyVar, param) {
71 if (param === undefined)
72 mhistory_dialog(undefined, historyVar);
73 else
74 mhistory_dialog(undefined, historyVar, param.width, param.height, param.x, param.y, param);
75}
76
77
78function mhistory_dialog(group, panel, width, height, x, y, param) {
79
80 // default minimal/initial width and height if not defined
81 if (width === undefined)
82 width = 400;
83 if (height === undefined)
84 height = 300;
85
86 let dlgFrame = document.createElement("div");
87 dlgFrame.className = "dlgFrame";
88 dlgFrame.style.zIndex = "30";
89 dlgFrame.style.backgroundColor = "white";
90 dlgFrame.style.overflow = "hidden";
91 dlgFrame.style.width = width + "px";
92 dlgFrame.style.height = height + "px";
93 dlgFrame.style.minWidth = "200px"; // overwrite max-content
94 dlgFrame.style.minHeight = "100px"; // overwrite max-content
95 dlgFrame.shouldDestroy = true;
96
97 let dlgTitle = document.createElement("div");
98 dlgTitle.className = "dlgTitlebar";
99 dlgTitle.id = "dlgMessageTitle";
100 if (panel.includes(":"))
101 dlgTitle.innerText = "History " + group;
102 else
103 dlgTitle.innerText = "History " + panel;
104 dlgFrame.appendChild(dlgTitle);
105 document.body.appendChild(dlgFrame);
106 dlgShow(dlgFrame);
107
108 let dlgPanel = document.createElement("div");
109 dlgPanel.className = "dlgPanel";
110 dlgPanel.style.flex = "1 1 auto";
111 dlgPanel.style.width = (width - 2) + "px";
112 dlgPanel.style.height = (height - dlgTitle.clientHeight - 2) + "px";
113 dlgPanel.style.minWidth = ""; // overwrite max-content
114 dlgPanel.style.minHeight = ""; // overwrite max-content
115 dlgPanel.style.padding = "0";
116 dlgPanel.style.border = "";
117 dlgPanel.style.backgroundColor = "transparent";
118 dlgFrame.appendChild(dlgPanel);
119
120 let dlgHistory = document.createElement("div");
121 dlgHistory.className = "mjshistory";
122 if (panel.includes(':')) {
123 dlgHistory.setAttribute("data-history-var", panel);
124 } else {
125 dlgHistory.setAttribute("data-group", group);
126 dlgHistory.setAttribute("data-panel", panel);
127 }
128
129 dlgHistory.style.height = "100%";
130 dlgPanel.appendChild(dlgHistory);
131
132 if (x !== undefined && y !== undefined)
133 dlgMove(dlgFrame, x, y);
134
135 dlgHistory.innerHTML = ""; // Needed to make sure of a fresh start
136 dlgHistory.mhg = new MhistoryGraph(dlgHistory, true, true);
137 dlgHistory.mhg.initializePanel(0, param);
138 dlgHistory.mhg.resize();
139
140 new ResizeObserver(() => {
141 const rect = dlgFrame.getBoundingClientRect();
142 dlgPanel.style.width = (rect.width - 4) + "px";
143 dlgPanel.style.height = (rect.height - dlgTitle.clientHeight - 4) + "px";
144
145 window.setTimeout(() => {
146 dlgHistory.mhg.width = parseInt(dlgPanel.style.width);
147 dlgHistory.mhg.height = parseInt(dlgPanel.style.height);
148 dlgHistory.mhg.canvas.width = dlgHistory.mhg.width;
149 dlgHistory.mhg.canvas.height = dlgHistory.mhg.height;
150
151 if (dlgHistory.mhg.intSelector !== undefined)
152 dlgHistory.mhg.intSelector.style.display = "none";
153
154 dlgHistory.mhg.forceConvert = true;
155 dlgHistory.mhg.redraw(true);
156 }, 10);
157 }).observe(dlgFrame);
158
159/*
160 // initialize history when resizing modal
161 const resizeObs = new ResizeObserver(() => {
162 dlgPanel.style.height = (100 * (dlgFrame.offsetHeight - dlgTitle.offsetHeight) / dlgFrame.offsetHeight) + "%";
163 mhistory_init(dlgHistory, true, param);
164 });
165 resizeObs.observe(dlgFrame);
166 // catch event when history dialog is closed
167 const observer = new MutationObserver(([{removedNodes}]) => {
168 if (removedNodes.length && removedNodes[0] === dlgFrame) {
169 resizeObs.unobserve(dlgFrame);
170 document.getElementById(dlgHistory.id + "intSel").remove();
171 document.getElementById(dlgHistory.id + "downloadSel").remove();
172 }
173 });
174 observer.observe(dlgFrame.parentNode, { childList: true });
175*/
176
177 return dlgFrame;
178}
179
180function mhistory_create(parentElement, baseURL, group, panel, tMin, tMax, index) {
181 let d = document.createElement("div");
182 parentElement.appendChild(d);
183 d.dataset.baseURL = baseURL;
184 d.dataset.group = group;
185 d.dataset.panel = panel;
186 d.mhg = new MhistoryGraph(d, undefined, false);
187 if (!Number.isNaN(tMin) && !Number.isNaN(tMax)) {
188 d.mhg.initTMin = tMin;
189 d.mhg.initTMax = tMax;
190 }
191 d.mhg.initializePanel(index);
192 return d;
193}
194
195function mhistory_create_var(parentElement, baseURL, historyVar, tMin, tMax, index) {
196 let d = document.createElement("div");
197 parentElement.appendChild(d);
198 d.dataset.baseURL = baseURL;
199 d.dataset.historyVar = historyVar;
200 d.mhg = new MhistoryGraph(d, undefined, true);
201 if (!Number.isNaN(tMin) && !Number.isNaN(tMax)) {
202 d.mhg.initTMin = tMin;
203 d.mhg.initTMax = tMax;
204 }
205 d.mhg.initializePanel(index);
206 return d;
207}
208
209function getUrlVars() {
210 let vars = {};
211 window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
212 vars[key] = value;
213 });
214 return vars;
215}
216
217function MhistoryGraph(divElement, noBorder, floating) { // Constructor
218
219 // create canvas inside the div
220 this.parentDiv = divElement;
221 // if absent, generate random string (5 char) to give an id to mhistory
222 if (!this.parentDiv.id)
223 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
224 this.baseURL = divElement.dataset.baseURL;
225 this.group = divElement.dataset.group;
226 this.panel = divElement.dataset.panel;
227 this.historyVar = divElement.dataset.historyVar;
228 this.floating = floating;
229 this.canvas = document.createElement("canvas");
230 if (noBorder !== true)
231 this.canvas.style.border = "1px solid black";
232 this.canvas.style.width = "100%";
233 this.canvas.style.height = "100%";
234 this.debugString = "";
235 divElement.appendChild(this.canvas);
236
237 // colors
238 this.color = {
239 background: "#FFFFFF",
240 axis: "#808080",
241 grid: "#D0D0D0",
242 label: "#404040",
243 data: [
244 "#00AAFF", "#FF9000", "#FF00A0", "#00C030",
245 "#A0C0D0", "#D0A060", "#C04010", "#807060",
246 "#F0C000", "#2090A0", "#D040D0", "#90B000",
247 "#B0B040", "#B0B0FF", "#FFA0A0", "#A0FFA0"],
248 };
249
250 // scales
251 this.tScale = 3600;
252 this.yMin0 = undefined;
253 this.yMax0 = undefined;
254 this.tMax = Math.floor(new Date() / 1000);
255 this.tMin = this.tMax - this.tScale;
256 this.yMin = undefined;
257 this.yMax = undefined;
258 this.scroll = true;
259 this.yZoom = false;
260 this.showZoomButtons = true;
261 this.showMenuButtons = true;
262 this.tMinRequested = 0;
263 this.tMinReceived = 0;
264 this.tMaxRequested = 0;
265 this.tMaxReceived = 0;
266 this.pinchW0 = undefined;
267 this.pinchH0 = undefined;
268
269 // overwrite scale from URL if present
270 let tMin = decodeURI(getUrlVars()["A"]);
271 if (tMin !== "undefined") {
272 this.initTMin = parseInt(tMin);
273 this.tMin = parseInt(tMin);
274 }
275 let tMax = decodeURI(getUrlVars()["B"]);
276 if (tMax !== "undefined") {
277 this.initTMax = parseInt(tMax);
278 this.tMax = parseInt(tMax);
279 }
280
281 // data arrays
282 this.data = [];
283 this.lastWritten = [];
284 this.binned = false;
285 this.binSize = 0;
286
287 // graph arrays (in screen pixels)
288 this.x = [];
289 this.y = [];
290 // t/v arrays corresponding to x/y
291 this.t = [];
292 this.v = [];
293 this.vRaw = [];
294
295 // points array with min/max/avg
296 this.p = [];
297
298 // dragging
299 this.drag = {
300 active: false,
301 lastT: 0,
302 lastOffsetX: 0,
303 lastDt: 0,
304 lastVt: 0,
305 lastMoveT : 0
306 };
307
308 // axis zoom
309 this.zoom = {
310 x: {active: false},
311 y: {active: false}
312 };
313
314 // callbacks when certain actions are performed.
315 // All callback functions should accept a single parameter, which is the
316 // MhistoryGraph object that triggered the callback.
317 this.callbacks = {
318 resetAxes: undefined,
319 timeZoom: undefined,
320 jumpToCurrent: undefined
321 };
322
323 // marker
324 this.marker = {active: false};
325 this.variablesWidth = 0;
326 this.variablesHeight = 0;
327
328 // labels
329 this.showLabels = false;
330
331 // axis
332 this.showAxis = true;
333
334 // title
335 this.showTitle = true;
336
337 // solo
338 this.solo = {active: false, index: undefined};
339
340 // time when panel was drawn last
341 this.lastDrawTime = 0;
342 this.forceRedraw = false;
343
344 // buttons
345 this.button = [
346 {
347 src: "maximize.svg",
348 title: "Show only this plot",
349 click: function (t) {
350 if (t.floating) {
351 let width = window.innerWidth-100;
352 let height = window.innerHeight-100;
353
354 t.parentDiv.parentNode.parentNode.style.width = width + "px";
355 t.parentDiv.parentNode.parentNode.style.height = height + "px";
356 dlgMove(t.parentDiv.parentNode.parentNode, 50, 50);
357 } else
358 window.location.href = t.baseURL + "&group=" + t.group + "&panel=" + t.panel;
359 }
360 },
361 {
362 src: "menu.svg",
363 title: "Show / hide legend",
364 click: function (t) {
365 t.showLabels = !t.showLabels;
366 t.redraw(true);
367 }
368 },
369 {
370 src: "rotate-ccw.svg",
371 title: "Reset histogram axes",
372 click: function (t) {
373 t.resetAxes();
374
375 if (t.callbacks.resetAxes !== undefined) {
376 t.callbacks.resetAxes(t);
377 }
378 }
379 },
380 {
381 src: "play.svg",
382 title: "Jump to current time",
383 click: function (t) {
384
385 let dt = Math.floor(t.tMax - t.tMin);
386
387 t.tMax = new Date() / 1000;
388 t.tMin = t.tMax - dt;
389 t.scroll = true;
390
391 t.loadFullData(t.tMin, t.tMax, true);
392
393 if (t.binned)
394 dlgAlert("Time range too large for automatic scrolling. Zoom in if you want automatic scrolling.");
395
396 if (t.callbacks.jumpToCurrent !== undefined) {
397 t.callbacks.jumpToCurrent(t);
398 }
399 }
400 },
401 {
402 src: "clock.svg",
403 title: "Select timespan...",
404 click: function (t) {
405 if (t.intSelector.style.display === "none") {
406 t.intSelector.style.display = "block";
407 t.intSelector.style.left = ((t.canvas.getBoundingClientRect().x + window.pageXOffset +
408 t.x2) - t.intSelector.offsetWidth) + "px";
409 t.intSelector.style.top = (t.canvas.getBoundingClientRect().y + window.pageYOffset +
410 this.y1 - 1) + "px";
411 t.intSelector.style.zIndex = "32";
412 } else {
413 t.intSelector.style.display = "none";
414 }
415 }
416 },
417 {
418 src: "download.svg",
419 title: "Download image/data...",
420 click: function (t) {
421 if (t.downloadSelector.style.display === "none") {
422 t.downloadSelector.style.display = "block";
423 t.downloadSelector.style.left = ((t.canvas.getBoundingClientRect().x + window.pageXOffset +
424 t.x2) - t.downloadSelector.offsetWidth) + "px";
425 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.pageYOffset +
426 this.y1 - 1) + "px";
427 t.downloadSelector.style.zIndex = "32";
428 } else {
429 t.downloadSelector.style.display = "none";
430 }
431 }
432 },
433 {
434 src: "settings.svg",
435 title: "Configure this plot",
436 click: function (t) {
437 window.location.href = "?cmd=hs_edit&group=" + encodeURIComponent(t.group) + "&panel=" + encodeURIComponent(t.panel) + "&redir=" + encodeURIComponent(window.location.href);
438 }
439 },
440 {
441 src: "help-circle.svg",
442 title: "Show help",
443 click: function () {
444 dlgShow("dlgHelp", false);
445 }
446 },
447 {
448 src: "corner-down-left.svg",
449 title: "Return to all variables",
450 click: function (t) {
451 t.solo.active = false;
452 t.findMinMax();
453 t.redraw();
454 }
455 }
456 ];
457
458 // remove settings for single variable plot
459 if (this.group === undefined) {
460 this.button.splice(1, 1);
461 this.button.splice(5, 1);
462 }
463
464 // load dialogs
465 dlgLoad('dlgHistory.html');
466
467 this.button.forEach(b => {
468 b.img = new Image();
469 b.img.src = "icons/" + b.src;
470 });
471
472 // marker
473 this.marker = {active: false};
474
475 // mouse event handlers
476 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
477 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
478 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
479
480 divElement.addEventListener("touchstart", this.mouseEvent.bind(this), true);
481 divElement.addEventListener("touchmove", this.mouseEvent.bind(this), true);
482 divElement.addEventListener("touchend", this.mouseEvent.bind(this), true);
483
484 divElement.addEventListener("wheel", this.mouseWheelEvent.bind(this), true);
485 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
486
487 // Keyboard event handler (has to be on the window!)
488 window.addEventListener("keydown", this.keyDown.bind(this));
489}
490
491function timeToSec(str) {
492 let s = parseFloat(str);
493 switch (str[str.length - 1]) {
494 case 'm':
495 case 'M':
496 s *= 60;
497 break;
498 case 'h':
499 case 'H':
500 s *= 3600;
501 break;
502 case 'd':
503 case 'D':
504 s *= 3600 * 24;
505 break;
506 }
507
508 return s;
509}
510
511function doQueryAB(t) {
512
513 dlgHide('dlgQueryAB');
514
515 let d1 = new Date(
516 document.getElementById('y1').value,
517 document.getElementById('m1').selectedIndex,
518 document.getElementById('d1').selectedIndex + 1,
519 document.getElementById('h1').selectedIndex);
520
521 let d2 = new Date(
522 document.getElementById('y2').value,
523 document.getElementById('m2').selectedIndex,
524 document.getElementById('d2').selectedIndex + 1,
525 document.getElementById('h2').selectedIndex);
526
527 if (d1 > d2)
528 [d1, d2] = [d2, d1];
529
530 t.scroll = false;
531
532 t.loadFullData(Math.floor(d1.getTime() / 1000), Math.floor(d2.getTime() / 1000));
533
534 if (t.callbacks.timeZoom !== undefined)
535 t.callbacks.timeZoom(t);
536}
537
538MhistoryGraph.prototype.keyDown = function (e) {
539 if (e.target && (e.target.isContentEditable || event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')) return;
540 if (e.key === "u") { // 'u' key
541 // force next update t.Min-dt/2 to current time
542 let dt = Math.floor(this.tMax - this.tMin);
543
544 // limit to one week maximum (otherwise we have to read binned data)
545 if (dt > 24*3600*7)
546 dt = 24*3600*7;
547
548 this.tMax = new Date() / 1000;
549 this.tMin = this.tMax - dt;
550 this.scroll = true;
551
552 this.loadFullData(this.tMin, this.tMax, true);
553
554 if (this.callbacks.jumpToCurrent !== undefined)
555 this.callbacks.jumpToCurrent(this);
556
557 e.preventDefault();
558 }
559 if (e.key === "r") { // 'r' key
560 this.resetAxes();
561 e.preventDefault();
562 }
563 if (e.key === "Escape") {
564 this.solo.active = false;
565 this.findMinMax();
566 this.redraw(true);
567 e.preventDefault();
568 }
569 if (e.key === "y") {
570 this.yZoom = false;
571 this.findMinMax();
572 this.redraw(true);
573 e.preventDefault();
574 }
575}
576
577MhistoryGraph.prototype.initializePanel = function (index, param) {
578
579 // initialize variables
580 this.plotIndex = index;
581 this.marker = {active: false};
582 this.drag = {active: false};
583 this.data = undefined;
584 this.x = [];
585 this.y = [];
586 this.events = [];
587 this.tags = [];
588 this.index = [];
589 this.pendingUpdates = 0;
590
591 // single variable mode
592 if (this.parentDiv.dataset.historyVar !== undefined) {
593 this.param = {
594 "Timescale": "1h",
595 "Minimum": 0,
596 "Maximum": 0,
597 "Zero ylow": false,
598 "Log axis": false,
599 "Show run markers": false,
600 "Show values": true,
601 "Show fill": true,
602 "Variables": this.parentDiv.dataset.historyVar.split(','),
603 "Formula": "",
604 "Show raw value": false
605 };
606
607 this.param.Label = new Array(this.param.Variables.length).fill('');
608 this.param.Colour = this.color.data.slice(0, this.param.Variables.length);
609
610 // optionally overwrite default parameters from argument
611 if (param !== undefined)
612 Object.keys(param).forEach(p => {
613 this.param[p] = param[p];
614 });
615
616 this.loadInitialData();
617
618 } else {
619
620 // ODB history panel mode
621 this.group = this.parentDiv.dataset.group;
622 this.panel = this.parentDiv.dataset.panel;
623
624 if (this.group === undefined) {
625 dlgMessage("Error", "Definition of \'dataset-group\' missing for history panel \'" + this.parentDiv.id + "\'. " +
626 "Please use syntax:<br /><br /><b>&lt;div class=\"mjshistory\" " +
627 "data-group=\"&lt;Group&gt;\" data-panel=\"&lt;Panel&gt;\"&gt;&lt;/div&gt;</b>", true);
628 return;
629 }
630 if (this.panel === undefined) {
631 dlgMessage("Error", "Definition of \'dataset-panel\' missing for history panel \'" + this.parentDiv.id + "\'. " +
632 "Please use syntax:<br /><br /><b>&lt;div class=\"mjshistory\" " +
633 "data-group=\"&lt;Group&gt;\" data-panel=\"&lt;Panel&gt;\"&gt;&lt;/div&gt;</b>", true);
634 return;
635 }
636
637 if (this.group === "" || this.panel === "")
638 return;
639
640 // retrieve panel definition from ODB
641 mjsonrpc_db_copy(["/History/Display/" + this.group + "/" + this.panel]).then(function (rpc) {
642 if (rpc.result.status[0] !== 1) {
643 dlgMessage("Error", "Panel \'" + this.group + "/" + this.panel + "\' not found in ODB", true)
644 } else {
645 let odb = rpc.result.data[0];
646 this.param = {};
647 this.param["Timescale"] = odb["Timescale"];
648 this.param["Minimum"] = odb["Minimum"];
649 this.param["Maximum"] = odb["Maximum"];
650 this.param["Zero ylow"] = odb["Zero ylow"];
651 this.param["Log axis"] = odb["Log axis"];
652 this.param["Show run markers"] = odb["Show run markers"];
653 this.param["Show values"] = odb["Show values"];
654 this.param["Show fill"] = odb["Show fill"];
655 this.param["Variables"] = odb["Variables"];
656 this.param["Formula"] = odb["Formula"];
657 this.param["Colour"] = odb["Colour"];
658 this.param["Label"] = odb["Label"];
659 this.param["Show raw value"] = odb["Show raw value"];
660
661 this.loadInitialData();
662 }
663 }.bind(this)).catch(function (error) {
664 if (error.xhr !== undefined)
665 mjsonrpc_error_alert(error);
666 else
667 throw (error);
668 });
669 }
670};
671
672MhistoryGraph.prototype.updateLastWritten = function () {
673 //console.log("update last_written!!!\n");
674
675 // load date of latest data points
676 mjsonrpc_call("hs_get_last_written",
677 {
678 "time": this.tMin,
679 "events": this.events,
680 "tags": this.tags,
681 "index": this.index
682 }).then(function (rpc) {
683 this.lastWritten = rpc.result.last_written;
684 // protect against an infinite loop from draw() if rpc returns invalid times.
685 // by definition, last_written returned by RPC is supposed to be less then tMin.
686 for (let i = 0; i < this.lastWritten.length; i++) {
687 let l = this.lastWritten[i];
688 //console.log("updated last_written: event: " + this.events[i] + ", l: " + l + ", tmin: " + this.tMin + ", diff: " + (l - this.tMin));
689 if (l > this.tMin) {
690 this.lastWritten[i] = this.tMin;
691 }
692 }
693 this.redraw(true);
694 }.bind(this))
695 .catch(function (error) {
696 mjsonrpc_error_alert(error);
697 });
698}
699
700MhistoryGraph.prototype.loadInitialData = function () {
701
702 if (this.initTMin !== undefined && this.initTMin !== "undefined") {
703 this.tMin = this.initTMin;
704 this.tMax = this.initTMax;
705 this.tScale = this.tMax - this.tMin;
706 this.scroll = false;
707 } else {
708 this.tScale = timeToSec(this.param["Timescale"]);
709
710 // overwrite via <div ... data-scale=<value> >
711 if (this.parentDiv.dataset.scale !== undefined)
712 this.tScale = timeToSec(this.parentDiv.dataset.scale);
713
714 this.tMax = Math.floor(new Date() / 1000);
715 this.tMin = this.tMax - this.tScale;
716 }
717
718 this.showLabels = this.param["Show values"];
719 this.showFill = this.param["Show fill"];
720
721 // overwrite parameters from <div data-xxx> tags
722 if (this.parentDiv.dataset.showValues !== undefined)
723 this.showLabels = this.parentDiv.dataset.showValues === "true" || this.parentDiv.dataset.showValues === "1";
724 if (this.parentDiv.dataset.showFill !== undefined)
725 this.showLabels = this.parentDiv.dataset.showFill === "true" || this.parentDiv.dataset.showFill === "1";
726 if (this.parentDiv.dataset.showAxis !== undefined)
727 this.showAxis = this.parentDiv.dataset.showAxis === "true" || this.parentDiv.dataset.showAxis === "1";
728 if (this.parentDiv.dataset.showTitle !== undefined)
729 this.showTitle = this.parentDiv.dataset.showTitle === "true" || this.parentDiv.dataset.showTitle === "1";
730 if (this.parentDiv.dataset.showMenuButtons !== undefined)
731 this.showMenuButtons = this.parentDiv.dataset.showMenuButtons === "true" || this.parentDiv.dataset.showMenuButtons === "1";
732 if (this.parentDiv.dataset.showZoomButtons !== undefined)
733 this.showZoomButtons = this.parentDiv.dataset.showZoomButtons === "true" || this.parentDiv.dataset.showZoomButtons === "1";
734
735 this.autoscaleMin = (this.param["Minimum"] === this.param["Maximum"] ||
736 this.param["Minimum"] === "-Infinity" || this.param["Minimum"] === "Infinity");
737 this.autoscaleMax = (this.param["Minimum"] === this.param["Maximum"] ||
738 this.param["Maximum"] === "-Infinity" || this.param["Maximum"] === "Infinity");
739
740 if (this.param["Zero ylow"]) {
741 this.autoscaleMin = false;
742 this.param["Minimum"] = 0;
743 }
744
745 this.logAxis = this.param["Log axis"];
746
747 // protect against empty history plot
748 if (!this.param.Variables) {
749 this.param.Variables = "(empty):(empty)";
750 this.param.Label = "(empty)";
751 this.param.Colour = "";
752 }
753
754 // if only one variable present, convert it to array[0]
755 if (!Array.isArray(this.param.Variables))
756 this.param.Variables = new Array(this.param.Variables);
757 if (!Array.isArray(this.param.Label))
758 this.param.Label = new Array(this.param.Label);
759 if (!Array.isArray(this.param.Colour))
760 this.param.Colour = new Array(this.param.Colour);
761
762 this.param["Variables"].forEach(v => {
763 let event_and_tag = splitEventAndTagName(v);
764 this.events.push(event_and_tag[0]);
765 let t = event_and_tag[1];
766 if (t.indexOf('[') !== -1) {
767 this.tags.push(t.substr(0, t.indexOf('[')));
768 this.index.push(parseInt(t.substr(t.indexOf('[') + 1)));
769 } else {
770 this.tags.push(t);
771 this.index.push(0);
772 }
773 });
774
775 if (this.param["Show run markers"]) {
776 this.events.push("Run transitions");
777 this.events.push("Run transitions");
778
779 this.tags.push("State");
780 this.tags.push("Run number");
781 this.index.push(0);
782 this.index.push(0);
783 }
784
785 // interval selector
786 let intSelId = this.parentDiv.id + "intSel";
787 if (document.getElementById(intSelId)) document.getElementById(intSelId).remove();
788 this.intSelector = document.createElement("div");
789 this.intSelector.id = intSelId;
790 this.intSelector.style.display = "none";
791 this.intSelector.style.position = "absolute";
792 this.intSelector.className = "mtable";
793 this.intSelector.style.borderRadius = "0";
794 this.intSelector.style.border = "2px solid #808080";
795 this.intSelector.style.margin = "0";
796 this.intSelector.style.padding = "0";
797 this.intSelector.style.left = "100px";
798 this.intSelector.style.top = "100px";
799
800 let table = document.createElement("table");
801 let row = null;
802 let cell;
803 let link;
804 let buttons = this.param["Buttons"];
805 if (buttons === undefined) {
806 buttons = [];
807 buttons.push("10m", "1h", "3h", "12h", "24h", "3d", "7d");
808 }
809 buttons.push("A&rarr;B");
810 buttons.push("&lt;&lt;&lt;");
811 buttons.push("&lt;&lt;");
812 buttons.forEach(function (b, i) {
813 if (i % 2 === 0)
814 row = document.createElement("tr");
815
816 cell = document.createElement("td");
817 cell.style.padding = "0";
818
819 link = document.createElement("a");
820 link.href = "#";
821 link.innerHTML = b;
822 if (b === "A&rarr;B")
823 link.title = "Display data between two dates";
824 else if (b === "&lt;&lt;")
825 link.title = "Go back in time to last available data";
826 else if (b === "&lt;&lt;&lt;")
827 link.title = "Go back in time to last available data for all variables on plot";
828 else
829 link.title = "Show last " + b;
830
831 let mhg = this;
832 link.onclick = function () {
833 if (b === "A&rarr;B") {
834 let currentYear = new Date().getFullYear();
835 let dMin = new Date(this.tMin * 1000);
836 let dMax = new Date(this.tMax * 1000);
837
838 if (document.getElementById('y1').length === 0) {
839 for (let i = currentYear; i > currentYear - 5; i--) {
840 let o = document.createElement('option');
841 o.value = i.toString();
842 o.appendChild(document.createTextNode(i.toString()));
843 document.getElementById('y1').appendChild(o);
844 o = document.createElement('option');
845 o.value = i.toString();
846 o.appendChild(document.createTextNode(i.toString()));
847 document.getElementById('y2').appendChild(o);
848 }
849 }
850
851 document.getElementById('m1').selectedIndex = dMin.getMonth();
852 document.getElementById('d1').selectedIndex = dMin.getDate() - 1;
853 document.getElementById('h1').selectedIndex = dMin.getHours();
854 document.getElementById('y1').selectedIndex = currentYear - dMin.getFullYear();
855
856 document.getElementById('m2').selectedIndex = dMax.getMonth();
857 document.getElementById('d2').selectedIndex = dMax.getDate() - 1;
858 document.getElementById('h2').selectedIndex = dMax.getHours();
859 document.getElementById('y2').selectedIndex = currentYear - dMax.getFullYear();
860
861 document.getElementById('dlgQueryQuery').onclick = function () {
862 doQueryAB(this);
863 }.bind(this);
864
865 dlgShow("dlgQueryAB");
866
867 } else if (b === "&lt;&lt;") {
868
869 mjsonrpc_call("hs_get_last_written",
870 {
871 "time": this.tMin,
872 "events": this.events,
873 "tags": this.tags,
874 "index": this.index
875 })
876 .then(function (rpc) {
877
878 let last = rpc.result.last_written[0];
879 for (let i = 0; i < rpc.result.last_written.length; i++) {
880 if (this.events[i] === "Run transitions") {
881 continue;
882 }
883 let l = rpc.result.last_written[i];
884 last = Math.max(last, l);
885 }
886
887 if (last !== 0) { // no data, at all!
888 let scale = mhg.tMax - mhg.tMin;
889 mhg.tMax = last + scale / 2;
890 mhg.tMin = last - scale / 2;
891
892 mhg.scroll = false;
893 mhg.marker.active = false;
894
895 mhg.loadFullData(mhg.tMin, mhg.tMax);
896
897 if (mhg.callbacks.timeZoom !== undefined)
898 mhg.callbacks.timeZoom(mhg);
899 }
900
901 }.bind(this))
902 .catch(function (error) {
903 mjsonrpc_error_alert(error);
904 });
905
906 } else if (b === "&lt;&lt;&lt;") {
907
908 mjsonrpc_call("hs_get_last_written",
909 {
910 "time": this.tMin,
911 "events": this.events,
912 "tags": this.tags,
913 "index": this.index
914 })
915 .then(function (rpc) {
916
917 let last = 0;
918 for (let i = 0; i < rpc.result.last_written.length; i++) {
919 let l = rpc.result.last_written[i];
920 if (this.events[i] === "Run transitions") {
921 continue;
922 }
923 if (last === 0) {
924 // no data for first variable
925 last = l;
926 } else if (l === 0) {
927 // no data for this variable
928 } else {
929 last = Math.min(last, l);
930 }
931 }
932 //console.log("last: " + last);
933
934 if (last !== 0) { // no data, at all!
935 let scale = mhg.tMax - mhg.tMin;
936 mhg.tMax = last + scale / 2;
937 mhg.tMin = last - scale / 2;
938
939 mhg.scroll = false;
940 mhg.marker.active = false;
941
942 mhg.loadFullData(mhg.tMin, mhg.tMax);
943
944 if (mhg.callbacks.timeZoom !== undefined)
945 mhg.callbacks.timeZoom(mhg);
946 }
947
948 }.bind(this))
949 .catch(function (error) {
950 mjsonrpc_error_alert(error);
951 });
952
953 } else {
954
955 mhg.tMax = new Date() / 1000;
956 mhg.tMin = mhg.tMax - timeToSec(b);
957 mhg.scroll = true;
958 mhg.loadFullData(mhg.tMin, mhg.tMax, true);
959 mhg.scrollRedraw();
960
961 if (mhg.callbacks.timeZoom !== undefined)
962 mhg.callbacks.timeZoom(mhg);
963 }
964 mhg.intSelector.style.display = "none";
965 return false;
966 }.bind(this);
967
968 cell.appendChild(link);
969 row.appendChild(cell);
970 if (i % 2 === 1)
971 table.appendChild(row);
972 }, this);
973
974 if (buttons.length % 2 === 1)
975 table.appendChild(row);
976
977 this.intSelector.appendChild(table);
978 document.body.appendChild(this.intSelector);
979
980 // download selector
981 let downloadSelId = this.parentDiv.id + "downloadSel";
982 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
983 this.downloadSelector = document.createElement("div");
984 this.downloadSelector.id = downloadSelId;
985 this.downloadSelector.style.display = "none";
986 this.downloadSelector.style.position = "absolute";
987 this.downloadSelector.className = "mtable";
988 this.downloadSelector.style.borderRadius = "0";
989 this.downloadSelector.style.border = "2px solid #808080";
990 this.downloadSelector.style.margin = "0";
991 this.downloadSelector.style.padding = "0";
992
993 this.downloadSelector.style.left = "100px";
994 this.downloadSelector.style.top = "100px";
995
996 table = document.createElement("table");
997 let mhg = this;
998
999 row = document.createElement("tr");
1000 cell = document.createElement("td");
1001 cell.style.padding = "0";
1002 link = document.createElement("a");
1003 link.href = "#";
1004 link.innerHTML = "CSV";
1005 link.title = "Download data in Comma Separated Value format";
1006 link.onclick = function () {
1007 mhg.downloadSelector.style.display = "none";
1008 mhg.download("CSV");
1009 return false;
1010 }.bind(this);
1011 cell.appendChild(link);
1012 row.appendChild(cell);
1013 table.appendChild(row);
1014
1015 row = document.createElement("tr");
1016 cell = document.createElement("td");
1017 cell.style.padding = "0";
1018 link = document.createElement("a");
1019 link.href = "#";
1020 link.innerHTML = "PNG";
1021 link.title = "Download image in PNG format";
1022 link.onclick = function () {
1023 mhg.downloadSelector.style.display = "none";
1024 mhg.download("PNG");
1025 return false;
1026 }.bind(this);
1027 cell.appendChild(link);
1028 row.appendChild(cell);
1029 table.appendChild(row);
1030
1031 this.downloadSelector.appendChild(table);
1032 document.body.appendChild(this.downloadSelector);
1033
1034 // load one window ahead in past and future
1035 this.loadFullData(this.tMin - this.tScale/2, this.tMax + this.tScale/2, this.scroll);
1036}
1037
1038MhistoryGraph.prototype.loadFullData = function (t1, t2, scrollFlag) {
1039
1040 // save data range
1041 this.tMin = t1;
1042 this.tMax = Math.min(t2, new Date() / 1000);
1043
1044 // retrieve binned data if we request eight hours or more
1045 this.binned = this.tMax - this.tMin >= 3600*8;
1046
1047 // don't update in binned mode
1048 if (this.binned)
1049 this.scroll = false;
1050
1051 // drop current data
1052 this.discardCurrentData();
1053
1054 // prevent future date
1055 let now = Math.floor(new Date() / 1000);
1056 if (t2 > now)
1057 t2 = now;
1058 if (t1 >= t2)
1059 t1 = t2 - 600;
1060
1061 this.tMaxRequested = t2;
1062 this.tMinRequested = t1;
1063
1064 if (this.binned) {
1065
1066 log_hs_read("loadFullData binned", t1, t2);
1067 this.parentDiv.style.cursor = "progress";
1068 this.pendingUpdates++;
1069 mjsonrpc_call("hs_read_binned_arraybuffer",
1070 {
1071 "start_time": t1,
1072 "end_time": t2,
1073 "num_bins": 5000,
1074 "events": this.events,
1075 "tags": this.tags,
1076 "index": this.index
1077 }, "arraybuffer")
1078 .then(function (rpc) {
1079
1080 this.tMinReceived = this.tMinRequested;
1081 this.tMaxReceived = this.tMaxRequested;
1082 this.pendingUpdates--;
1083
1084 this.receiveDataBinned(rpc);
1085 this.findMinMax();
1086 this.redraw(true);
1087
1088 this.parentDiv.style.cursor = "default";
1089
1090 }.bind(this))
1091 .catch(function (error) {
1092 mjsonrpc_error_alert(error);
1093 });
1094
1095 } else {
1096
1097 // limit one request to maximum one month
1098 if (t2 - t1 > 3600 * 24 * 30) {
1099 t1 = (t1 + t2)/2 - 3600 * 24 * 15;
1100 t2 = (t1 + t2)/2 + 3600 * 24 * 15;
1101 let now = Math.floor(new Date() / 1000);
1102 if (t2 > now)
1103 t2 = now;
1104 }
1105
1106 log_hs_read("loadFullData un-binned", t1, t2);
1107 this.parentDiv.style.cursor = "progress";
1108 this.pendingUpdates++;
1109 mjsonrpc_call("hs_read_arraybuffer",
1110 {
1111 "start_time": Math.floor(this.tMinRequested),
1112 "end_time": Math.floor(this.tMaxRequested),
1113 "events": this.events,
1114 "tags": this.tags,
1115 "index": this.index
1116 }, "arraybuffer")
1117 .then(function (rpc) {
1118
1119 this.tMinReceived = this.tMinRequested;
1120 this.tMaxReceived = this.tMaxRequested;
1121 this.pendingUpdates--;
1122
1123 this.receiveData(rpc);
1124 this.findMinMax();
1125 this.redraw(true);
1126
1127 if (scrollFlag)
1128 this.loadNewData(); // triggers scrolling
1129
1130 this.parentDiv.style.cursor = "default";
1131
1132 }.bind(this))
1133 .catch(function (error) {
1134 mjsonrpc_error_alert(error);
1135 });
1136 }
1137}
1138
1139MhistoryGraph.prototype.loadSideData = function () {
1140
1141 let dt = this.tMaxReceived - this.tMinReceived;
1142 let t1, t2;
1143
1144 // check for left side data
1145 if (this.tMin < this.tMinRequested) {
1146
1147 t1 = this.tMin - dt; // request one window
1148 t2 = this.tMinReceived;
1149 this.tMinRequested = t1;
1150
1151 if (this.binned) {
1152 log_hs_read("loadSideData left binned", t1, t2);
1153 this.parentDiv.style.cursor = "progress";
1154 this.pendingUpdates++;
1155 mjsonrpc_call("hs_read_binned_arraybuffer",
1156 {
1157 "start_time": t1,
1158 "end_time": t2,
1159 "num_bins": 5000,
1160 "events": this.events,
1161 "tags": this.tags,
1162 "index": this.index
1163 }, "arraybuffer")
1164 .then(function (rpc) {
1165
1166 this.tMinReceived = this.tMinRequested;
1167 this.pendingUpdates--;
1168
1169 this.receiveDataBinned(rpc);
1170 this.findMinMax();
1171 this.redraw(true);
1172
1173 this.parentDiv.style.cursor = "default";
1174
1175 }.bind(this))
1176 .catch(function (error) {
1177 mjsonrpc_error_alert(error);
1178 });
1179
1180 } else { // un-binned
1181
1182 log_hs_read("loadSideData left un-binned", t1, t2);
1183 this.pendingUpdates++;
1184 mjsonrpc_call("hs_read_arraybuffer",
1185 {
1186 "start_time": t1,
1187 "end_time": t2,
1188 "events": this.events,
1189 "tags": this.tags,
1190 "index": this.index
1191 }, "arraybuffer")
1192 .then(function (rpc) {
1193
1194 this.tMinReceived = this.tMinRequested;
1195 this.pendingUpdates--;
1196
1197 this.receiveData(rpc);
1198 this.findMinMax();
1199 this.redraw(true);
1200
1201 this.parentDiv.style.cursor = "default";
1202
1203 }.bind(this))
1204 .catch(function (error) {
1205 mjsonrpc_error_alert(error);
1206 });
1207 }
1208 }
1209
1210 // check for right side data
1211 if (this.tMax > this.tMaxRequested) {
1212
1213 t1 = this.tMaxReceived - 2;
1214 t2 = this.tMax + dt; // request one window
1215 this.tMaxRequested = t2;
1216
1217 if (this.binned) {
1218 log_hs_read("loadSideData right binned", t1, t2);
1219 this.parentDiv.style.cursor = "progress";
1220 this.pendingUpdates++;
1221 mjsonrpc_call("hs_read_binned_arraybuffer",
1222 {
1223 "start_time": t1,
1224 "end_time": t2,
1225 "num_bins": 5000,
1226 "events": this.events,
1227 "tags": this.tags,
1228 "index": this.index
1229 }, "arraybuffer")
1230 .then(function (rpc) {
1231
1232 // this.tMaxReceived = this.tMaxRequested; --> updated in receiveDataBinned()
1233 this.pendingUpdates--;
1234
1235 this.receiveDataBinned(rpc);
1236 this.findMinMax();
1237 this.redraw(true);
1238
1239 this.parentDiv.style.cursor = "default";
1240
1241 }.bind(this))
1242 .catch(function (error) {
1243 mjsonrpc_error_alert(error);
1244 });
1245
1246 } else { // un-binned
1247
1248 this.parentDiv.style.cursor = "progress";
1249 log_hs_read("loadSideData right un-binned", t1, t2);
1250 this.pendingUpdates++;
1251 mjsonrpc_call("hs_read_arraybuffer",
1252 {
1253 "start_time": t1,
1254 "end_time": t2,
1255 "events": this.events,
1256 "tags": this.tags,
1257 "index": this.index
1258 }, "arraybuffer")
1259 .then(function (rpc) {
1260
1261 // this.tMaxReceived = this.tMaxRequested; --> updated in receiveData()
1262 this.pendingUpdates--;
1263
1264 this.receiveData(rpc);
1265 this.findMinMax();
1266 this.redraw(true);
1267
1268 this.parentDiv.style.cursor = "default";
1269
1270 }.bind(this))
1271 .catch(function (error) {
1272 mjsonrpc_error_alert(error);
1273 });
1274 }
1275 }
1276};
1277
1278MhistoryGraph.prototype.receiveData = function (rpc) {
1279
1280 // decode binary array
1281 let array = new Float64Array(rpc);
1282 let nVars = array[1];
1283 let nData = array.slice(2 + nVars, 2 + 2 * nVars);
1284 let i = 2 + 2 * nVars;
1285
1286 if (array.length <= i) {
1287 // RPC did not return any data
1288
1289 if (this.data === undefined) {
1290 // must initialize the arrays otherwise nothing works
1291 this.data = [];
1292 for (let index = 0; index < nVars; index++) {
1293 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1294 }
1295 }
1296
1297 return false;
1298 }
1299
1300 // bin size 1 for un-binned data
1301 this.binSize = 1;
1302
1303 // push empty arrays on the first time
1304 if (this.data === undefined) {
1305 this.data = [];
1306 for (let index = 0; index < nVars; index++) {
1307 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1308 }
1309 }
1310
1311 // append new values to end of arrays
1312 for (let index = 0; index < nVars; index++) {
1313 if (nData[index] === 0)
1314 continue;
1315
1316 let formula = this.param["Formula"];
1317 if (Array.isArray(formula))
1318 formula = formula[index];
1319
1320 let t1 = [];
1321 let v1 = [];
1322 let v1Raw = [];
1323 let x, v, t;
1324 if (formula !== undefined && formula !== "") {
1325 for (let j = 0; j < nData[index]; j++) {
1326 t = array[i++];
1327 x = array[i++];
1328 v = eval(formula);
1329 t1.push(t);
1330 v1.push(v);
1331 v1Raw.push(x);
1332 }
1333 } else {
1334 for (let j = 0; j < nData[index]; j++) {
1335 t = array[i++];
1336 v = array[i++];
1337 t1.push(t);
1338 v1.push(v);
1339 }
1340 }
1341
1342 if (t1.length > 0) {
1343
1344 let da = new Date(t1[0]*1000);
1345 let db = new Date(t1[t1.length-1]*1000);
1346
1347 if (index === 0)
1348 log_hs_read("receiveData un-binned", t1[0], t1[t1.length-1]);
1349
1350 // Update tMaxReceived with last time in array.
1351 // This solves the problem if the server time is off
1352 // by a few seconds, preventing proper updates
1353 if (t1[t1.length-1] > this.tMaxReceived)
1354 this.tMaxReceived = t1[t1.length-1];
1355
1356 let told = this.data[index].time;
1357
1358 if (this.data[index].time.length === 0 ||
1359 t1[t1.length-1] >= this.data[index].time[this.data[index].time.length-1]) {
1360
1361 // remove overlapping data
1362 if (this.data[index].time.length > 0)
1363 while (t1[0] <= this.data[index].time[this.data[index].time.length-1]) {
1364 t1 = t1.slice(1);
1365 v1 = v1.slice(1);
1366 if (v1Raw.length > 0)
1367 v1Raw = v1Raw.slice(1);
1368 }
1369
1370 // add data to the right
1371 this.data[index].time = this.data[index].time.concat(t1);
1372 this.data[index].value = this.data[index].value.concat(v1);
1373 if (v1Raw.length > 0)
1374 this.data[index].rawValue = this.data[index].rawValue.concat(v1Raw);
1375
1376 } else {
1377
1378 // strip overlapping data
1379 while (t1[t1.length-1] >= this.data[index].time[0]) {
1380 t1.pop();
1381 v1.pop();
1382 if (v1Raw.length > 0)
1383 v1Raw.pop();
1384 }
1385
1386 // add data to the left
1387 this.data[index].time = t1.concat(this.data[index].time);
1388 this.data[index].value = v1.concat(this.data[index].value);
1389 if (v1Raw.length > 0)
1390 this.data[index].rawValue = v1Raw.concat(this.data[index].rawValue);
1391 }
1392
1393 if (index === 0) {
1394 for (let i = 1; i < this.data[index].time.length; i++)
1395 if (this.data[index].time[i] < this.data[index].time[i - 1]) {
1396 console.log("Error non-continuous data");
1397 log_hs_read("told", told[0], told[told.length-1]);
1398 log_hs_read("t1", t1[0], t1[t1.length-1]);
1399 }
1400 }
1401 }
1402 }
1403
1404 return true;
1405}
1406
1407MhistoryGraph.prototype.receiveDataBinned = function (rpc) {
1408
1409 // decode binary array
1410 let array = new Float64Array(rpc);
1411
1412 // let status = array[0];
1413 // let startTime = array[1];
1414 // let endTime = array[2];
1415 let numBins = array[3];
1416 let nVars = array[4];
1417
1418 let i = 5;
1419 // let hsStatus = array.slice(i, i + nVars);
1420 i += nVars;
1421 let numEntries = array.slice(i, i + nVars);
1422 i += nVars;
1423 // let lastTime = array.slice(i, i + nVars);
1424 i += nVars;
1425 // let lastValue = array.slice(i, i + nVars);
1426 i += nVars;
1427
1428 if (i >= array.length) {
1429 // RPC did not return any data
1430
1431 if (this.data === undefined) {
1432 // must initialize the arrays otherwise nothing works
1433 this.data = [];
1434 for (let index = 0; index < nVars; index++) {
1435 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1436 }
1437 }
1438
1439 return false;
1440 }
1441
1442 // push empty arrays on the first time
1443 if (this.data === undefined) {
1444 this.data = [];
1445 for (let index = 0; index < nVars; index++) {
1446 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1447 }
1448 }
1449
1450 let binSize = 0;
1451 let binSizeN = 0;
1452
1453 // create arrays of new values
1454 for (let index = 0; index < nVars; index++) {
1455 if (numEntries[index] === 0)
1456 continue;
1457
1458 let t1 = [];
1459 let bin1 = [];
1460 let binRaw1 = [];
1461
1462 // add data to the right
1463 let formula = this.param["Formula"];
1464 if (Array.isArray(formula))
1465 formula = formula[index];
1466
1467 if (formula === undefined || formula === "") {
1468 for (let j = 0; j < numBins; j++) {
1469
1470 let count = array[i++];
1471 // let mean = array[i++];
1472 // let rms = array[i++];
1473 i += 2;
1474 let minValue = array[i++];
1475 let maxValue = array[i++];
1476 let firstTime = array[i++];
1477 let firstValue = array[i++];
1478 let lastTime = array[i++];
1479 let lastValue = array[i++];
1480 let t = Math.floor((firstTime + lastTime) / 2);
1481
1482 if (count > 0) {
1483 // append to the right
1484 t1.push(t);
1485
1486 let bin = {};
1487 bin.count = count;
1488 bin.firstValue = firstValue;
1489 bin.lastValue = lastValue;
1490 bin.minValue = minValue;
1491 bin.maxValue = maxValue;
1492
1493 bin1.push(bin);
1494
1495 // calculate average bin count
1496 binSize += count;
1497 binSizeN++;
1498 }
1499 }
1500
1501 } else { // use formula
1502
1503 for (let j = 0; j < numBins; j++) {
1504
1505 let count = array[i++];
1506 // let mean = array[i++];
1507 // let rms = array[i++];
1508 i += 2;
1509 let minValue = array[i++];
1510 let maxValue = array[i++];
1511 let firstTime = array[i++];
1512 let firstValue = array[i++];
1513 let lastTime = array[i++];
1514 let lastValue = array[i++];
1515
1516 if (count > 0) {
1517 // append to the right
1518 t1.push(Math.floor((firstTime + lastTime) / 2));
1519
1520 let bin = {};
1521 let binRaw = {};
1522 let x = firstValue;
1523 binRaw.firstValue = firstValue;
1524 bin.firstValue = eval(formula);
1525 x = lastValue;
1526 binRaw.lastValue = lastValue;
1527 bin.lastValue = eval(formula);
1528 x = minValue;
1529 binRaw.minValue = minValue;
1530 bin.minValue = eval(formula);
1531 x = maxValue;
1532 binRaw.maxValue = maxValue;
1533 bin.maxValue = eval(formula);
1534
1535 bin1.push(bin);
1536 binRaw1.push(binRaw);
1537
1538 // calculate average bin count
1539 binSize += count;
1540 binSizeN++;
1541 }
1542 }
1543 }
1544
1545 if (t1.length > 0) {
1546
1547 let da = new Date(t1[0]*1000);
1548 let db = new Date(t1[t1.length-1]*1000);
1549
1550 if (index === 0)
1551 log_hs_read("receiveData binned", t1[0], t1[t1.length-1]);
1552
1553 // Update tMaxReceived with last time in array.
1554 // This solves the problem if the server time is off
1555 // by a few seconds, preventing proper updates
1556 if (t1[t1.length-1] > this.tMaxReceived)
1557 this.tMaxReceived = t1[t1.length-1];
1558
1559 if (this.data[index].time.length === 0 ||
1560 t1[0] > this.data[index].time[0]) {
1561
1562 // append to right if new data
1563 this.data[index].time = this.data[index].time.concat(t1);
1564 this.data[index].bin = this.data[index].bin.concat(bin1);
1565 if (binRaw1.length > 0)
1566 this.data[index].binRaw = this.data[index].rawValue.concat(binRaw1);
1567
1568 } else {
1569
1570 // append to left if old data
1571 this.data[index].time = t1.concat(this.data[index].time);
1572 this.data[index].bin = bin1.concat(this.data[index].bin);
1573 if (binRaw1.length > 0)
1574 this.data[index].binRaw = binRaw1.concat(this.data[index].binRaw);
1575 }
1576 }
1577 }
1578
1579 // calculate average bin size
1580 if (binSizeN > 0)
1581 this.binSize = binSize / binSizeN;
1582
1583 return true;
1584};
1585
1586MhistoryGraph.prototype.loadNewData = function () {
1587
1588 if (this.updateTimer)
1589 window.clearTimeout(this.updateTimer);
1590
1591 // don't update window if content is hidden (other tab, minimized, etc.)
1592 if (document.hidden) {
1593 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1594 return;
1595 }
1596
1597 // don't update if not in scrolling mode
1598 if (!this.scroll) {
1599 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1600 return;
1601 }
1602
1603 // update data from last point to current time
1604 let t1 = this.tMaxReceived;
1605 if (t1 === undefined || t1 === 0)
1606 t1 = this.tMin;
1607 t1 -= 2; // add two seconds of overlap in case any history data arrived late
1608 let t2 = Math.floor(new Date() / 1000);
1609
1610 // for strip-chart mode always use non-binned data
1611 this.binned = false;
1612
1613 log_hs_read("loadNewData un-binned", t1, t2);
1614 mjsonrpc_call("hs_read_arraybuffer",
1615 {
1616 "start_time": Math.floor(t1),
1617 "end_time": Math.floor(t2),
1618 "events": this.events,
1619 "tags": this.tags,
1620 "index": this.index
1621 }, "arraybuffer")
1622 .then(function (rpc) {
1623
1624 if (this.tMinRequested === undefined || t1 < this.tMinRequested) {
1625 this.tMinRequested = t1;
1626 this.tMinReceived = t1;
1627 }
1628
1629 if (this.tMaxRequested === undefined || t2 > this.tMaxRequested) {
1630 // this.tMaxReceived = t2; -> updated with last received array element
1631 this.tMaxRequested = t2;
1632 }
1633
1634 if (this.receiveData(rpc)) {
1635 this.findMinMax();
1636 this.scrollRedraw();
1637 }
1638
1639 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 1000);
1640
1641 }.bind(this)).catch(function (error) {
1642 mjsonrpc_error_alert(error);
1643 });
1644}
1645
1646MhistoryGraph.prototype.scrollRedraw = function () {
1647 if (this.scrollTimer)
1648 window.clearTimeout(this.scrollTimer);
1649
1650 if (this.scroll) {
1651 let dt = this.tMax - this.tMin;
1652 this.tMax = new Date() / 1000;
1653 this.tMin = this.tMax - dt;
1654 this.findMinMax();
1655 this.redraw(true);
1656
1657 // calculate time for one pixel
1658 dt = (this.tMax - this.tMin) / (this.x2 - this.x1);
1659 dt = Math.min(Math.max(0.1, dt), 60);
1660 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), dt / 2 * 1000);
1661 } else {
1662 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
1663 this.redraw(true);
1664 }
1665}
1666
1667MhistoryGraph.prototype.discardCurrentData = function () {
1668
1669 if (this.data === undefined)
1670 return;
1671
1672 // drop all data
1673 for (let i = 0 ; i<this.data.length ; i++) {
1674 this.data[i].time.length = 0;
1675 if (this.data[i].value)
1676 this.data[i].value.length = 0;
1677 if (this.data[i].bin)
1678 this.data[i].bin.length = 0;
1679 if (this.data[i].rawBin)
1680 this.data[i].rawBin.length = 0;
1681 if (this.data[i].rawValue)
1682 this.data[i].rawValue.length = 0;
1683
1684 this.x[i].length = 0;
1685 this.y[i].length = 0;
1686 this.t[i].length = 0;
1687 this.v[i].length = 0;
1688 if (this.vRaw[i])
1689 this.vRaw[i].length = 0;
1690 }
1691
1692 this.tMaxReceived = undefined ;
1693 this.tMaxRequested = undefined;
1694 this.tMinRequested = undefined;
1695 this.tMinReceived = undefined;
1696}
1697
1698function binarySearch(array, target) {
1699 let startIndex = 0;
1700 let endIndex = array.length - 1;
1701 let middleIndex;
1702 while (startIndex <= endIndex) {
1703 middleIndex = Math.floor((startIndex + endIndex) / 2);
1704 if (target === array[middleIndex])
1705 return middleIndex;
1706
1707 if (target > array[middleIndex])
1708 startIndex = middleIndex + 1;
1709 if (target < array[middleIndex])
1710 endIndex = middleIndex - 1;
1711 }
1712
1713 return middleIndex;
1714}
1715
1716function splitEventAndTagName(var_name) {
1717 let colons = [];
1718
1719 for (let i = 0; i < var_name.length; i++) {
1720 if (var_name[i] == ':') {
1721 colons.push(i);
1722 }
1723 }
1724
1725 let slash_pos = var_name.indexOf("/");
1726 let uses_per_variable_naming = (slash_pos != -1);
1727
1728 if (uses_per_variable_naming && colons.length % 2 == 1) {
1729 let middle_colon_pos = colons[Math.floor(colons.length / 2)];
1730 let slash_to_mid = var_name.substr(slash_pos + 1, middle_colon_pos - slash_pos - 1);
1731 let mid_to_end = var_name.substr(middle_colon_pos + 1);
1732
1733 if (slash_to_mid == mid_to_end) {
1734 // Special case - we have a string of the form Beamlime/GS2:FC1:GS2:FC1.
1735 // Logger has already warned people that having colons in the equipment/event
1736 // names is a bad idea, so we only need to worry about them in the tag name.
1737 split_pos = middle_colon_pos;
1738 } else {
1739 // We have a string of the form Beamlime/Demand:GS2:FC1. Split at the first colon.
1740 split_pos = colons[0];
1741 }
1742 } else {
1743 // Normal case - split at the fist colon.
1744 split_pos = colons[0];
1745 }
1746
1747 let event_name = var_name.substr(0, split_pos);
1748 let tag_name = var_name.substr(split_pos + 1);
1749
1750 return [event_name, tag_name];
1751}
1752
1753MhistoryGraph.prototype.mouseEvent = function (e) {
1754
1755 // fix buttons for IE
1756 if (!e.which && e.button) {
1757 if ((e.button & 1) > 0) e.which = 1; // Left
1758 else if ((e.button & 4) > 0) e.which = 2; // Middle
1759 else if ((e.button & 2) > 0) e.which = 3; // Right
1760 }
1761
1762 // extract X and Y coordinates
1763 let eventX, eventY;
1764
1765 if (e.type === "touchstart" || e.type === "touchmove") {
1766 eventX = e.touches[0].clientX;
1767 eventY = e.touches[0].clientY;
1768 let rect = e.target.getBoundingClientRect();
1769 eventX = eventX - Math.round(rect.left);
1770 eventY = eventY - Math.round(rect.top);
1771 } else if (e.type === "mousedown" || e.type === "mousemove" || e.type === "mouseup" ||
1772 e.type === "dblclick") {
1773 eventX = e.offsetX;
1774 eventY = e.offsetY;
1775 }
1776
1777 let cursor = this.pendingUpdates > 0 ? "progress" : "";
1778 let title = "";
1779 let cancel = false;
1780
1781 // cancel dragging in case we did not catch the mouseup event
1782 if (e.type === "mousemove" && e.buttons === 0 &&
1783 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
1784 cancel = true;
1785
1786 if (e.type === "mousedown" || (e.type === "touchstart" && e.touches.length === 1)) {
1787
1788 this.intSelector.style.display = "none";
1789 this.downloadSelector.style.display = "none";
1790
1791 // check for buttons
1792 this.button.forEach(b => {
1793 if (eventX > b.x1 && eventX < b.x1 + b.width &&
1794 eventY > b.y1 && eventY < b.y1 + b.width &&
1795 b.enabled) {
1796 b.click(this);
1797 }
1798 });
1799
1800 // check for zoom buttons
1801 let xb;
1802 if (this.showMenuButtons)
1803 xb = this.width - 26 - 40;
1804 else
1805 xb = this.width - 41;
1806 if (eventX > xb && eventX < xb + 20 &&
1807 eventY > this.y1 - 20 && eventY < this.y1) {
1808 // zoom in
1809 let delta = this.tMax - this.tMin;
1810 if (this.scroll) {
1811 this.tMin += delta / 2; // only zoom on left side in scroll mode
1812 } else {
1813 this.tMin += delta / 4;
1814 this.tMax -= delta / 4; // zoom to center
1815 }
1816
1817 this.loadFullData(this.tMin, this.tMax);
1818
1819 if (this.callbacks.timeZoom !== undefined)
1820 this.callbacks.timeZoom(this);
1821
1822 e.preventDefault();
1823 return;
1824 }
1825 if (eventX > xb + 20 && eventX < xb + 40 &&
1826 eventY > this.y1 - 20 && eventY < this.y1) {
1827 // zoom out
1828 if (this.pendingUpdates > 0) {
1829 dlgMessage("Warning", "Don't press the '-' too fast!", true, false);
1830 } else {
1831 let delta = this.tMax - this.tMin;
1832 this.tMin -= delta / 2;
1833 this.tMax += delta / 2;
1834 // don't go into the future
1835 let now = Math.floor(new Date() / 1000);
1836 if (this.tMax > now) {
1837 this.tMax = now;
1838 this.tMin = now - 2*delta;
1839 }
1840
1841 this.loadFullData(this.tMin, this.tMax);
1842
1843 if (this.callbacks.timeZoom !== undefined)
1844 this.callbacks.timeZoom(this);
1845 }
1846
1847 if (e.type === "mousedown" ) {
1848 e.preventDefault();
1849 return;
1850 }
1851 }
1852
1853 // check for dragging
1854 if (eventX > this.x1 && eventX < this.x2 &&
1855 eventY > this.y2 && eventY < this.y1) {
1856 this.drag.active = true;
1857 this.marker.active = false;
1858 this.drag.xStart = eventX;
1859 this.drag.yStart = eventY;
1860 this.drag.tStart = this.xToTime(eventX);
1861 this.drag.tMinStart = this.tMin;
1862 this.drag.tMaxStart = this.tMax;
1863 this.drag.yMinStart = this.yMin;
1864 this.drag.yMaxStart = this.yMax;
1865 this.drag.vStart = this.yToValue(eventY);
1866 }
1867
1868 // check for axis dragging
1869 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1 && eventY < this.y1 + 20) {
1870 this.zoom.x.active = true;
1871 this.scroll = false;
1872 this.zoom.x.x1 = eventX;
1873 this.zoom.x.x2 = undefined;
1874 this.zoom.x.t1 = this.xToTime(eventX);
1875 }
1876 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1 && eventX > this.x1 - 30) {
1877 this.zoom.y.active = true;
1878 this.scroll = false;
1879 this.zoom.y.y1 = eventY;
1880 this.zoom.y.y2 = undefined;
1881 this.zoom.y.v1 = this.yToValue(eventY);
1882 }
1883
1884 }
1885
1886 if (cancel || e.type === "mouseup" || e.type === "touchend") {
1887
1888 if (this.drag.active) {
1889 this.drag.active = false;
1890 }
1891
1892 if (this.zoom.x.active) {
1893 if (this.zoom.x.x2 !== undefined &&
1894 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
1895 let t1 = this.zoom.x.t1;
1896 let t2 = this.xToTime(this.zoom.x.x2);
1897 if (t1 > t2)
1898 [t1, t2] = [t2, t1];
1899 if (t2 - t1 < 1)
1900 t1 -= 1;
1901 this.tMin = t1;
1902 this.tMax = t2;
1903 }
1904 this.zoom.x.active = false;
1905
1906 this.loadFullData(this.tMin, this.tMax);
1907
1908 if (this.callbacks.timeZoom !== undefined)
1909 this.callbacks.timeZoom(this);
1910 }
1911
1912 if (this.zoom.y.active) {
1913 if (this.zoom.y.y2 !== undefined &&
1914 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
1915 let v1 = this.zoom.y.v1;
1916 let v2 = this.yToValue(this.zoom.y.y2);
1917 if (v1 > v2)
1918 [v1, v2] = [v2, v1];
1919 this.yMin = v1;
1920 this.yMax = v2;
1921 }
1922 this.zoom.y.active = false;
1923 this.yZoom = true;
1924 this.findMinMax();
1925 this.redraw(true);
1926 }
1927
1928 }
1929
1930 if (e.type === "touchstart" && e.touches.length === 2) {
1931
1932 // stop scrolling
1933 this.scroll = false;
1934
1935 // start pinch / zoom
1936
1937 let rect = e.target.getBoundingClientRect();
1938
1939 this.zoom.x.pinch = true;
1940 this.zoom.x.x1 = e.touches[0].clientX - Math.round(rect.left);
1941 this.zoom.x.x2 = e.touches[1].clientX - Math.round(rect.left);
1942 this.zoom.x.t1 = this.xToTime(this.zoom.x.x1);
1943 this.zoom.x.t2 = this.xToTime(this.zoom.x.x2);
1944
1945 this.zoom.y.pinch = true;
1946 this.zoom.y.y1 = e.touches[0].clientY - Math.round(rect.top);
1947 this.zoom.y.y2 = e.touches[1].clientY - Math.round(rect.top);
1948 this.zoom.y.v1 = this.yToValue(this.zoom.y.y1);
1949 this.zoom.y.v2 = this.yToValue(this.zoom.y.y2);
1950
1951 let w = Math.abs(this.zoom.x.x2 - this.zoom.x.x1);
1952 let h = Math.abs(this.zoom.y.y2 - this.zoom.y.y1);
1953
1954 if (w < h/4)
1955 this.zoom.x.pinch = false;
1956 if (h < w/4)
1957 this.zoom.y.pinch = false;
1958
1959 if (this.zoom.y.pinch)
1960 this.yZoom = true;
1961
1962 }
1963
1964 if (e.type === "touchmove" && e.touches.length === 2) {
1965
1966 // pinch / zoom
1967
1968 let rect = e.target.getBoundingClientRect();
1969 let x1 = e.touches[0].clientX - Math.round(rect.left);
1970 let x2 = e.touches[1].clientX - Math.round(rect.left);
1971 let y1 = e.touches[0].clientY - Math.round(rect.top);
1972 let y2 = e.touches[1].clientY - Math.round(rect.top);
1973
1974 // solution to linear equation:
1975 // xToTime(x1) =!= this.zoom.x.t1
1976 // xToTime(x2) =!= this.zoom.x.t2
1977
1978 if (this.zoom.x.pinch) {
1979 let a = (x1 - this.x1) / (this.x2 - this.x1);
1980 let b = (x2 - this.x1) / (this.x2 - this.x1);
1981
1982 let tMin = (a * this.zoom.x.t2 - b * this.zoom.x.t1) / (a - b);
1983 let tMax = ((a - 1) * this.zoom.x.t2 - (b - 1) * this.zoom.x.t1) / (a - b);
1984
1985 if (tMax > tMin + 10) {
1986 this.tMin = tMin;
1987 this.tMax = tMax;
1988 }
1989
1990 this.loadSideData();
1991 }
1992
1993 if (this.zoom.y.pinch) {
1994 let a = (this.y1 - y1) / (this.y1 - this.y2);
1995 let b = (this.y1 - y2) / (this.y1 - this.y2);
1996
1997 let yMin = (a * this.zoom.y.v2 - b * this.zoom.y.v1) / (a - b);
1998 let yMax = ((a - 1) * this.zoom.y.v2 - (b - 1) * this.zoom.y.v1) / (a - b);
1999
2000 if (yMax > yMin) {
2001 this.yMin = yMin;
2002 this.yMax = yMax;
2003 }
2004 }
2005
2006 this.redraw();
2007 }
2008
2009 if (e.type === "touchend") {
2010 if (this.zoom.x.pinch)
2011 this.loadFullData(this.tMin, this.tMax);
2012
2013 this.zoom.x.pinch = false;
2014 this.zoom.y.pinch = false;
2015 }
2016
2017 if (e.type === "mousemove" || ((e.type === "touchmove" || e.type === "touchstart") && e.touches.length === 1) ) {
2018
2019 if (this.drag.active) {
2020
2021 // stop scrolling
2022 this.scroll = false;
2023
2024 // execute dragging
2025 cursor = "move";
2026 let dt = Math.round((eventX - this.drag.xStart) / (this.x2 - this.x1) * (this.tMax - this.tMin));
2027 this.tMin = this.drag.tMinStart - dt;
2028 this.tMax = this.drag.tMaxStart - dt;
2029 this.drag.lastDt = (eventX - this.drag.lastOffsetX) / (this.x2 - this.x1) * (this.tMax - this.tMin);
2030 this.drag.lastT = new Date().getTime();
2031 this.drag.lastOffsetX = eventX;
2032
2033 if (this.yZoom) {
2034
2035 if (this.logAxis) {
2036
2037 let dy = eventY - this.drag.yStart;
2038
2039 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));
2040 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));
2041
2042 if (this.yMin <= 0)
2043 this.yMin = 1E-20;
2044 if (this.yMax <= 0)
2045 this.yMax = 1E-18;
2046
2047 } else {
2048 let dy = (this.drag.yStart - eventY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
2049 this.yMin = this.drag.yMinStart - dy;
2050 this.yMax = this.drag.yMaxStart - dy;
2051 }
2052
2053 }
2054
2055 this.loadSideData();
2056 this.findMinMax();
2057 this.redraw();
2058
2059 if (this.callbacks.timeZoom !== undefined)
2060 this.callbacks.timeZoom(this);
2061
2062 }
2063
2064 if (!this.drag.active || e.type === "touchstart" || e.type === "touchmove") {
2065
2066 let redraw = false;
2067
2068 // change cursor to pointer over buttons
2069 this.button.forEach(b => {
2070 if (eventX > b.x1 && eventY > b.y1 &&
2071 eventX < b.x1 + b.width && eventY < b.y1 + b.height) {
2072 cursor = "pointer";
2073 title = b.title;
2074 }
2075 });
2076
2077 if (this.showZoomButtons) {
2078
2079 let xb;
2080 if (this.showMenuButtons)
2081 xb = this.width - 26 - 40;
2082 else
2083 xb = this.width - 41;
2084
2085 // check for zoom buttons
2086 if (eventX > xb && eventX < xb + 20 &&
2087 eventY > this.y1 - 20 && eventY < this.y1) {
2088 cursor = "pointer";
2089 title = "Zoom in";
2090 }
2091 if (eventX > xb + 20 && eventX < xb + 40 &&
2092 eventY > this.y1 - 20 && eventY < this.y1) {
2093 cursor = "pointer";
2094 title = "Zoom out";
2095 }
2096 }
2097
2098 // display zoom cursor
2099 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1 && eventY < this.y1 + 20)
2100 cursor = "col-resize";
2101 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1 && eventX > this.x1 - 30)
2102 cursor = "row-resize";
2103
2104 // execute axis zoom
2105 if (this.zoom.x.active) {
2106 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, eventX));
2107 this.zoom.x.t2 = this.xToTime(eventX);
2108 redraw = true;
2109 }
2110 if (this.zoom.y.active) {
2111 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, eventY));
2112 this.zoom.y.v2 = this.yToValue(eventY);
2113 redraw = true;
2114 }
2115
2116 // check if cursor close to graph point
2117 if (this.data !== undefined && this.x.length && this.y.length) {
2118
2119 let minDist = 10000;
2120 let markerX, markerY, markerT, markerV;
2121 for (let di = 0; di < this.data.length; di++) {
2122
2123 if (this.solo.active && di !== this.solo.index)
2124 continue;
2125
2126 let i1 = binarySearch(this.x[di], eventX - 10);
2127 let i2 = binarySearch(this.x[di], eventX + 10);
2128
2129 if (!this.binned) {
2130 for (let i = i1; i <= i2; i++) {
2131 let d = (eventX - this.x[di][i]) * (eventX - this.x[di][i]) +
2132 (eventY - this.y[di][i]) * (eventY - this.y[di][i]);
2133 if (d < minDist) {
2134 minDist = d;
2135 markerX = this.x[di][i];
2136 markerY = this.y[di][i];
2137 markerT = this.t[di][i];
2138 markerV = this.v[di][i];
2139
2140 if (this.param["Show raw value"] !== undefined &&
2141 this.param["Show raw value"][di])
2142 markerV = this.vRaw[di][i];
2143 else
2144 markerV = this.v[di][i];
2145
2146 this.marker.graphIndex = di;
2147 this.marker.index = i;
2148 }
2149 }
2150 } else {
2151
2152 // check max values
2153 for (let i = i1; i <= i2; i++) {
2154 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2155 (eventY - this.p[di][i].max) * (eventY - this.p[di][i].max);
2156 if (d < minDist) {
2157 minDist = d;
2158 markerX = this.p[di][i].x;
2159 markerY = this.p[di][i].max;
2160 markerT = this.p[di][i].t;
2161
2162 if (this.param["Show raw value"] !== undefined &&
2163 this.param["Show raw value"][di])
2164 markerV = this.p[di][i].rawMaxValue;
2165 else
2166 markerV = this.p[di][i].maxValue;
2167
2168 this.marker.graphIndex = di;
2169 this.marker.index = i;
2170 }
2171 }
2172
2173 // check min values
2174 for (let i = i1; i <= i2; i++) {
2175 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2176 (eventY - this.p[di][i].min) * (eventY - this.p[di][i].min);
2177 if (d < minDist) {
2178 minDist = d;
2179 markerX = this.p[di][i].x;
2180 markerY = this.p[di][i].min;
2181 markerT = this.p[di][i].t;
2182
2183 if (this.param["Show raw value"] !== undefined &&
2184 this.param["Show raw value"][di])
2185 markerV = this.p[di][i].rawMinValue;
2186 else
2187 markerV = this.p[di][i].minValue;
2188
2189 this.marker.graphIndex = di;
2190 this.marker.index = i;
2191 }
2192 }
2193
2194 }
2195 }
2196
2197 // exclude zoom buttons if visible
2198 let exclude = false;
2199 if (this.showZoomButtons &&
2200 eventX > this.width - 26 - 40 && this.offsetX < this.width - 26 &&
2201 eventY > this.y1 - 20 && eventY < this.y1) {
2202 exclude = true;
2203 }
2204 // exclude label area
2205 if (this.showLabels &&
2206 eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7 &&
2207 eventY > this.y2 && eventY < this.y2 + this.variablesHeight + 2) {
2208 exclude = true;
2209 }
2210
2211 if (exclude) {
2212 this.marker.active = false;
2213 } else {
2214 this.marker.active = Math.sqrt(minDist) < 10 && eventX > this.x1 && eventX < this.x2;
2215 if (this.marker.active) {
2216 this.marker.x = markerX;
2217 this.marker.y = markerY;
2218 this.marker.t = markerT;
2219 this.marker.v = markerV;
2220
2221 this.marker.mx = eventX;
2222 this.marker.my = eventY;
2223 }
2224 }
2225 if (this.marker.active)
2226 redraw = true;
2227 if (!this.marker.active && this.marker.activeOld)
2228 redraw = true;
2229 this.marker.activeOld = this.marker.active;
2230
2231 if (redraw)
2232 this.redraw(true);
2233 }
2234 }
2235
2236 }
2237
2238 if (e.type === "dblclick") {
2239
2240 // check if inside zoom buttons
2241 if (eventX > this.width - 26 - 40 && eventX < this.width - 26 &&
2242 eventY > this.y1 - 20 && eventY < this.y1) {
2243 // just ignore it
2244
2245 } else {
2246
2247 // measure distance to graphs
2248 if (this.data !== undefined && this.x.length && this.y.length) {
2249
2250 // check if inside label area
2251 let flag = false;
2252 if (this.showLabels) {
2253 if (eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7) {
2254 let i = Math.floor((eventY - (this.y2 + 4)) / 17);
2255 if (i < this.data.length) {
2256 this.solo.active = true;
2257 this.solo.index = i;
2258 this.findMinMax();
2259 flag = true;
2260 }
2261 }
2262 }
2263
2264 if (!flag) {
2265 let minDist = 100;
2266 for (let di = 0; di < this.data.length; di++) {
2267 for (let i = 0; i < this.x[di].length; i++) {
2268 if (this.x[di][i] > this.x1 && this.x[di][i] < this.x2) {
2269 let d = Math.sqrt(Math.pow(eventX - this.x[di][i], 2) +
2270 Math.pow(eventY - this.y[di][i], 2));
2271 if (d < minDist) {
2272 minDist = d;
2273 this.solo.index = di;
2274 }
2275 }
2276 }
2277 }
2278 // check if close to graph point
2279 if (minDist < 10 && eventX > this.x1 && eventX < this.x2)
2280 this.solo.active = !this.solo.active;
2281 this.findMinMax();
2282 }
2283
2284 this.redraw(true);
2285 }
2286 }
2287 }
2288
2289 this.parentDiv.title = title;
2290 this.parentDiv.style.cursor = cursor;
2291
2292 e.preventDefault();
2293};
2294
2295MhistoryGraph.prototype.mouseWheelEvent = function (e) {
2296
2297 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2298 e.offsetY > this.y2 && e.offsetY < this.y1) {
2299
2300 if (e.altKey || e.shiftKey) {
2301
2302 // zoom Y axis
2303 this.yZoom = true;
2304 let f = (e.offsetY - this.y1) / (this.y2 - this.y1);
2305
2306 let step = e.deltaY / 100;
2307 if (step > 0.5)
2308 step = 0.5;
2309 if (step < -0.5)
2310 step = -0.5;
2311
2312 let dtMin = f * (this.yMax - this.yMin) * step;
2313 let dtMax = (1 - f) * (this.yMax - this.yMin) * step;
2314
2315 if (((this.yMax + dtMax) - (this.yMin - dtMin)) / (this.yMax0 - this.yMin0) < 1000 &&
2316 (this.yMax0 - this.yMin0) / ((this.yMax + dtMax) - (this.yMin - dtMin)) < 1000) {
2317 this.yMin -= dtMin;
2318 this.yMax += dtMax;
2319
2320 if (this.logAxis && this.yMin <= 0)
2321 this.yMin = 1E-20;
2322 if (this.logAxis && this.yMax <= 0)
2323 this.yMax = 1E-18;
2324 }
2325
2326 this.redraw();
2327
2328 } else if (e.ctrlKey || e.metaKey) {
2329
2330 this.showZoomButtons = false;
2331
2332 // zoom time axis
2333 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2334 let m = e.deltaY / 100;
2335 if (m > 0.3)
2336 m = 0.3;
2337 if (m < -0.3)
2338 m = -0.3;
2339 let dtMin = Math.abs(f * (this.tMax - this.tMin) * m);
2340 let dtMax = Math.abs((1 - f) * (this.tMax - this.tMin) * m);
2341
2342 if (e.deltaY < 0) {
2343 // zoom in
2344 if (this.scroll) {
2345 this.tMin += dtMin;
2346 } else {
2347 this.tMin += dtMin;
2348 this.tMax -= dtMax;
2349 }
2350
2351 this.loadFullData(this.tMin, this.tMax);
2352 }
2353 if (e.deltaY > 0) {
2354 // zoom out
2355 if (this.scroll) {
2356 this.tMin -= dtMin;
2357 } else {
2358 this.tMin -= dtMin;
2359 this.tMax += dtMax;
2360 }
2361
2362 this.loadFullData(this.tMin, this.tMax);
2363 }
2364
2365 if (this.callbacks.timeZoom !== undefined)
2366 this.callbacks.timeZoom(this);
2367
2368 } else if (e.deltaX !== 0) {
2369
2370 let dt = (this.tMax - this.tMin) / 1000 * e.deltaX;
2371 this.tMin += dt;
2372 this.tMax += dt;
2373
2374 if (dt < 0)
2375 this.loadSideData();
2376 this.findMinMax();
2377 this.redraw();
2378 } else
2379 return;
2380
2381 this.marker.active = false;
2382
2383 e.preventDefault();
2384 }
2385};
2386
2387MhistoryGraph.prototype.resetAxes = function () {
2388 this.tMax = Math.floor(new Date() / 1000);
2389 this.tMin = this.tMax - this.tScale;
2390
2391 this.scroll = true;
2392 this.yZoom = false;
2393 this.showZoomButtons = true;
2394 this.loadFullData(this.tMin, this.tMax);
2395};
2396
2397MhistoryGraph.prototype.jumpToCurrent = function () {
2398 let dt = Math.floor(this.tMax - this.tMin);
2399
2400 // limit to one week maximum (otherwise we have to read binned data)
2401 if (dt > 24*3600*7)
2402 dt = 24*3600*7;
2403
2404 this.tMax = Math.floor(new Date() / 1000);
2405 this.tMin = this.tMax - dt;
2406 this.scroll = true;
2407
2408 this.loadFullData(this.tMin, this.tMax);
2409};
2410
2411MhistoryGraph.prototype.setTimespan = function (tMin, tMax, scroll) {
2412 this.scroll = scroll;
2413
2414 this.loadFullData(tMin, tMax, scroll);
2415};
2416
2417MhistoryGraph.prototype.resize = function () {
2418 this.canvas.width = this.parentDiv.clientWidth;
2419 this.canvas.height = this.parentDiv.clientHeight;
2420 this.width = this.parentDiv.clientWidth;
2421 this.height = this.parentDiv.clientHeight;
2422
2423 if (this.intSelector !== undefined)
2424 this.intSelector.style.display = "none";
2425
2426 this.forceConvert = true;
2427 this.redraw(true);
2428};
2429
2430MhistoryGraph.prototype.redraw = function (force) {
2431 this.forceRedraw = force;
2432 let f = this.draw.bind(this);
2433 window.requestAnimationFrame(f);
2434};
2435
2436MhistoryGraph.prototype.timeToXInit = function () {
2437 this.timeToXScale = 1 / (this.tMax - this.tMin) * (this.x2 - this.x1);
2438}
2439
2440MhistoryGraph.prototype.timeToX = function (t) {
2441 return (t - this.tMin) * this.timeToXScale + this.x1;
2442};
2443
2444MhistoryGraph.prototype.truncateInfinity = function(v) {
2445 if (v === Infinity) {
2446 return Number.MAX_VALUE;
2447 } else if (v === -Infinity) {
2448 return -Number.MAX_VALUE;
2449 } else {
2450 return v;
2451 }
2452};
2453
2454MhistoryGraph.prototype.valueToYInit = function () {
2455 // Avoid overflow of max - min > inf
2456 let max_scaled = this.yMax / 1e4;
2457 let min_scaled = this.yMin / 1e4;
2458 this.valueToYScale = (this.y1 - this.y2) * 1e-4 / (max_scaled - min_scaled);
2459}
2460
2461MhistoryGraph.prototype.valueToY = function (v) {
2462 if (v === Infinity) {
2463 return this.yMax >= Number.MAX_VALUE ? this.y2 : 0;
2464 } else if (v === -Infinity) {
2465 return this.yMin <= -Number.MAX_VALUE ? this.y1 : this.y1 * 2;
2466 } else if (this.logAxis) {
2467 if (v <= 0)
2468 return this.y1;
2469 else
2470 return this.y1 - (Math.log(v) - Math.log(this.yMin)) /
2471 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
2472 } else {
2473 return this.y1 - (v - this.yMin) * this.valueToYScale;
2474 }
2475};
2476
2477MhistoryGraph.prototype.xToTime = function (x) {
2478 return (x - this.x1) / (this.x2 - this.x1) * (this.tMax - this.tMin) + this.tMin;
2479};
2480
2481MhistoryGraph.prototype.yToValue = function (y) {
2482 if (!isFinite(this.yMax - this.yMin)) {
2483 // Contortions to avoid Infinity.
2484 let scaled = (this.yMax / 1e4) - (this.yMin / 1e4);
2485 let retval = ((((this.y1 - y) / (this.y1 - this.y2)) * scaled) + (this.yMin / 1e4)) * 1e4;
2486 return retval;
2487 }
2488 if (this.logAxis) {
2489 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
2490 return Math.exp(yl);
2491 }
2492 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
2493};
2494
2495MhistoryGraph.prototype.findMinMax = function () {
2496
2497 if (this.yZoom)
2498 return;
2499
2500 if (!this.autoscaleMin)
2501 this.yMin0 = this.param["Minimum"];
2502
2503 if (!this.autoscaleMax)
2504 this.yMax0 = this.param["Maximum"];
2505
2506 if (!this.autoscaleMin && !this.autoscaleMax) {
2507 this.yMin = this.yMin0;
2508 this.yMax = this.yMax0;
2509 return;
2510 }
2511
2512 let minValue = undefined;
2513 let maxValue = undefined;
2514 for (let index = 0; index < this.data.length; index++) {
2515 if (this.events[index] === "Run transitions")
2516 continue;
2517 if (this.data[index].time.length === 0)
2518 continue;
2519 if (this.solo.active && this.solo.index !== index)
2520 continue;
2521 let i1 = binarySearch(this.data[index].time, this.tMin) + 1;
2522 let i2 = binarySearch(this.data[index].time, this.tMax);
2523 while ((minValue === undefined ||
2524 maxValue === undefined ||
2525 Number.isNaN(minValue) ||
2526 Number.isNaN(maxValue)) &&
2527 i1 < i2) {
2528 // find first valid value
2529 if (this.binned) {
2530 if (this.data[index].bin[i1].count !== 0) {
2531 minValue = this.data[index].bin[i1].minValue;
2532 maxValue = this.data[index].bin[i1].maxValue;
2533 }
2534 } else {
2535 minValue = this.data[index].value[i1];
2536 maxValue = this.data[index].value[i1];
2537 }
2538 i1++;
2539 }
2540 for (let i = i1; i <= i2; i++) {
2541 if (this.binned) {
2542 if (this.data[index].bin[i].count === 0)
2543 continue;
2544 let v = this.data[index].bin[i].minValue;
2545 if (v < minValue)
2546 minValue = v;
2547 v = this.data[index].bin[i].maxValue;
2548 if (v > maxValue)
2549 maxValue = v;
2550 } else {
2551 let v = this.data[index].value[i];
2552 if (Number.isNaN(v))
2553 continue;
2554 if (v < minValue)
2555 minValue = v;
2556 if (v > maxValue)
2557 maxValue = v;
2558 }
2559 }
2560 }
2561
2562 // array could be empty (no data), so min/max would be NaN
2563 if (Number.isNaN(minValue) || Number.isNaN(maxValue))
2564 minValue = maxValue = 0;
2565
2566 if (this.autoscaleMin)
2567 this.yMin0 = this.yMin = minValue;
2568 if (this.autoscaleMax)
2569 this.yMax0 = this.yMax = maxValue;
2570
2571 if (minValue === undefined || maxValue === undefined) {
2572 this.yMin0 = -0.5;
2573 this.yMax0 = 0.5;
2574 }
2575
2576 if (this.yMin0 === this.yMax0) {
2577 this.yMin0 -= 0.5;
2578 this.yMax0 += 0.5;
2579 }
2580
2581 if (this.yMax0 < this.yMin0)
2582 this.yMax0 = this.yMin0 + 1;
2583
2584 if (!this.yZoom) {
2585 if (this.autoscaleMin) {
2586 if (this.logAxis)
2587 this.yMin = 0.8 * this.yMin0;
2588 else
2589 // leave 10% space below graph
2590 this.yMin = this.yMin0 - this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2591 } else
2592 this.yMin = this.yMin0;
2593 if (this.logAxis && this.yMin <= 0)
2594 this.yMin = 1E-20;
2595
2596 if (this.autoscaleMax) {
2597 if (this.logAxis)
2598 this.yMax = 1.2 * this.yMax0;
2599 else
2600 // leave 10% space above graph
2601 this.yMax = this.yMax0 + this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2602 } else
2603 this.yMax = this.yMax0;
2604 if (this.logAxis && this.yMax <= 0)
2605 this.yMax = 1E-18;
2606 }
2607
2608 this.yMax = this.truncateInfinity(this.yMax)
2609 this.yMin = this.truncateInfinity(this.yMin)
2610};
2611
2612function convertLastWritten(last) {
2613 if (last === 0)
2614 return "no data available";
2615
2616 let d = new Date(last * 1000).toLocaleDateString(
2617 'en-GB', {
2618 day: '2-digit', month: 'short', year: '2-digit',
2619 hour12: false, hour: '2-digit', minute: '2-digit'
2620 }
2621 );
2622
2623 return "last data: " + d;
2624}
2625
2626MhistoryGraph.prototype.updateURL = function() {
2627 let url = window.location.href;
2628 if (url.search("&A=") !== -1)
2629 url = url.slice(0, url.search("&A="));
2630 url += "&A=" + Math.round(this.tMin) + "&B=" + Math.round(this.tMax);
2631
2632 if (url !== window.location.href)
2633 window.history.replaceState({}, "Midas History", url);
2634}
2635
2636function createPinstripeCanvas() {
2637 const patternCanvas = document.createElement("canvas");
2638 const pctx = patternCanvas.getContext('2d', { antialias: true });
2639 const colour = "#FFC0C0";
2640
2641 const CANVAS_SIDE_LENGTH = 90;
2642 const WIDTH = CANVAS_SIDE_LENGTH;
2643 const HEIGHT = CANVAS_SIDE_LENGTH;
2644 const DIVISIONS = 4;
2645
2646 patternCanvas.width = WIDTH;
2647 patternCanvas.height = HEIGHT;
2648 pctx.fillStyle = colour;
2649
2650 // Top line
2651 pctx.beginPath();
2652 pctx.moveTo(0, HEIGHT * (1 / DIVISIONS));
2653 pctx.lineTo(WIDTH * (1 / DIVISIONS), 0);
2654 pctx.lineTo(0, 0);
2655 pctx.lineTo(0, HEIGHT * (1 / DIVISIONS));
2656 pctx.fill();
2657
2658 // Middle line
2659 pctx.beginPath();
2660 pctx.moveTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2661 pctx.lineTo(WIDTH * (1 / DIVISIONS), HEIGHT);
2662 pctx.lineTo(0, HEIGHT);
2663 pctx.lineTo(0, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2664 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), 0);
2665 pctx.lineTo(WIDTH, 0);
2666 pctx.lineTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2667 pctx.fill();
2668
2669 // Bottom line
2670 pctx.beginPath();
2671 pctx.moveTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2672 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), HEIGHT);
2673 pctx.lineTo(WIDTH, HEIGHT);
2674 pctx.lineTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2675 pctx.fill();
2676
2677 return patternCanvas;
2678}
2679
2680MhistoryGraph.prototype.draw = function () {
2681 //profile(true);
2682
2683 // draw maximal 30 times per second
2684 if (!this.forceRedraw) {
2685 if (new Date().getTime() < this.lastDrawTime + 30)
2686 return;
2687 this.lastDrawTime = new Date().getTime();
2688 }
2689 this.forceRedraw = false;
2690
2691 let update_last_written = false;
2692
2693 let ctx = this.canvas.getContext("2d");
2694
2695 ctx.fillStyle = this.color.background;
2696 ctx.fillRect(0, 0, this.width, this.height);
2697
2698 if (this.data === undefined) {
2699 ctx.lineWidth = 1;
2700 ctx.font = "14px sans-serif";
2701 ctx.strokeStyle = "#808080";
2702 ctx.fillStyle = "#808080";
2703 ctx.textAlign = "center";
2704 ctx.textBaseline = "middle";
2705 ctx.fillText("Data being loaded ...", this.width / 2, this.height / 2);
2706 return;
2707 }
2708
2709 ctx.lineWidth = 1;
2710 ctx.font = "14px sans-serif";
2711
2712 if (this.height === undefined || this.width === undefined)
2713 return;
2714 if (this.yMin === undefined || Number.isNaN(this.yMin))
2715 return;
2716 if (this.yMax === undefined || Number.isNaN(this.yMax))
2717 return;
2718
2719 let axisLabelWidth = this.drawVAxis(ctx, 50, this.height - 25, this.height - 35,
2720 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.logAxis, false);
2721
2722 if (axisLabelWidth === undefined)
2723 return;
2724
2725 if (this.showAxis) {
2726 this.x1 = axisLabelWidth + 15;
2727 this.y1 = this.height - 25;
2728 this.x2 = this.width - 26;
2729 this.y2 = 10;
2730 } else {
2731 this.x1 = 1;
2732 this.y1 = this.height - 1;
2733 this.x2 = this.width - 26;
2734 this.y2 = 1;
2735 }
2736
2737 if (this.showMenuButtons === false)
2738 this.x2 = this.width - 1;
2739
2740 // title
2741 if (!this.floating && // suppress title since this is already in the dialog box
2742 this.showTitle) {
2743 this.y2 = 26;
2744 ctx.strokeStyle = this.color.axis;
2745 ctx.fillStyle = "#F0F0F0";
2746 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, 20);
2747 ctx.fillRect(this.x1, 6, this.x2 - this.x1, 20);
2748 ctx.textAlign = "center";
2749 ctx.textBaseline = "middle";
2750 ctx.fillStyle = "#808080";
2751 let str = "";
2752 if (this.group !== undefined)
2753 str += this.group + " - " + this.panel;
2754 else if (this.historyVar !== undefined)
2755 str += this.historyVar;
2756
2757 if (this.debugString !== "")
2758 str += " - " + this.debugString;
2759
2760 ctx.fillText(str, (this.x2 + this.x1) / 2, 16);
2761
2762 // display binning
2763 let s = Math.round(this.binSize);
2764 ctx.textAlign = "right";
2765 ctx.fillText(s, this.x2 - 10, 16);
2766 }
2767
2768 // re-calculate axis scaling since x2, y2 might have been changed above
2769 this.timeToXInit(); // initialize scale factor t -> x
2770 this.valueToYInit(); // initialize scale factor v -> y
2771
2772 // draw axis
2773 ctx.strokeStyle = this.color.axis;
2774 ctx.drawLine(this.x1, this.y2, this.x2, this.y2);
2775 ctx.drawLine(this.x2, this.y2, this.x2, this.y1);
2776
2777 if (this.logAxis && this.yMin < 1E-20)
2778 this.yMin = 1E-20;
2779 if (this.logAxis && this.yMax < 1E-18)
2780 this.yMax = 1E-18;
2781 this.drawVAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
2782 -4, -7, -10, -12, this.x2 - this.x1, this.yMin, this.yMax, this.logAxis, true);
2783 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
2784 4, 7, 10, 10, this.y2 - this.y1, this.tMin, this.tMax);
2785
2786 // draw hatched area for "future"
2787 let t = new Date() / 1000;
2788 if (this.tMax > t) {
2789 let x = this.timeToX(t);
2790 if (x < this.x1)
2791 x = this.x1;
2792 ctx.fillStyle = ctx.createPattern(createPinstripeCanvas(), 'repeat');
2793 ctx.fillRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2794
2795 ctx.strokeStyle = this.color.axis;
2796 ctx.strokeRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2797 }
2798
2799 // determine precision
2800 let n_sig1, n_sig2;
2801 if (this.yMin === 0)
2802 n_sig1 = 1;
2803 else
2804 n_sig1 = Math.floor(Math.log(Math.abs(this.yMin)) / Math.log(10)) -
2805 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2806
2807 if (this.yMax === 0)
2808 n_sig2 = 1;
2809 else
2810 n_sig2 = Math.floor(Math.log(Math.abs(this.yMax)) / Math.log(10)) -
2811 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2812
2813 n_sig1 = Math.max(n_sig1, n_sig2);
2814 n_sig1 = Math.max(1, n_sig1);
2815
2816 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2817 if (Math.abs(this.yMin) < 100000)
2818 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMin)) /
2819 Math.log(10) + 0.001) + 1);
2820 if (Math.abs(this.yMax) < 100000)
2821 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMax)) /
2822 Math.log(10) + 0.001) + 1);
2823
2824 if (isNaN(n_sig1))
2825 n_sig1 = 6;
2826
2827 this.yPrecision = Math.max(6, n_sig1); // use at least 5 digits
2828
2829 ctx.save();
2830 ctx.beginPath();
2831 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2832 ctx.clip();
2833
2834 //profile("drawinit");
2835
2836 let nPoints = 0;
2837 for (let di = 0; di < this.data.length; di++)
2838 nPoints += this.data[di].time.length;
2839
2840 // convert values to points if window has changed or number of points have changed
2841 if (this.tMin !== this.tMinOld || this.tMax !== this.tMaxOld ||
2842 this.yMin !== this.yMinOld || this.yMax !== this.yMaxOld ||
2843 nPoints !== this.nPointsOld || this.forceConvert) {
2844
2845 this.tMinOld = this.tMin;
2846 this.tMaxOld = this.tMax;
2847 this.yMinOld = this.yMin;
2848 this.yMaxOld = this.yMax;
2849 this.nPointsOld = nPoints;
2850 this.forceConvert = false;
2851
2852 //profile();
2853 for (let di = 0; di < this.data.length; di++) {
2854
2855 if (this.x[di] === undefined) {
2856 this.x[di] = []; // x/y contain visible part of graph
2857 this.y[di] = [];
2858 this.t[di] = []; // t/v contain time/value pairs corresponding to x/y
2859 this.v[di] = [];
2860 this.p[di] = [];
2861 this.vRaw[di] = []; // vRaw contains the value before the formula
2862 }
2863
2864 let n = 0;
2865
2866 if (this.data[di].time.length === 0)
2867 continue;
2868
2869 let i1 = binarySearch(this.data[di].time, this.tMin);
2870 if (i1 > 0)
2871 i1--; // add point to the left
2872 let i2 = binarySearch(this.data[di].time, this.tMax);
2873 if (i2 < this.data[di].time.length - 1)
2874 i2++; // add points to the right
2875
2876 // un-binned data
2877 if (!this.binned || this.events[di] === "Run transitions") {
2878 for (let i = i1; i <= i2; i++) {
2879 let x = this.timeToX(this.data[di].time[i]);
2880 let y = this.valueToY(this.data[di].value[i]);
2881 if (y < -100000)
2882 y = -100000;
2883 if (y > 100000)
2884 y = 100000;
2885 if (!Number.isNaN(y)) {
2886 this.x[di][n] = x;
2887 this.y[di][n] = y;
2888 this.t[di][n] = this.data[di].time[i];
2889 this.v[di][n] = this.data[di].value[i];
2890 if (this.data[di].rawValue)
2891 this.vRaw[di][n] = this.data[di].rawValue[i];
2892 n++;
2893 }
2894 }
2895
2896 // truncate arrays if now shorter
2897 this.x[di].length = n;
2898 this.y[di].length = n;
2899 this.t[di].length = n;
2900 this.v[di].length = n;
2901 if (this.data[di].rawValue)
2902 this.vRaw[di].length = n;
2903
2904 } else {
2905
2906 // binned data
2907 for (let i = i1; i <= i2; i++) {
2908
2909 if (this.data[di].bin.length === 0 || this.data[di].bin[i].count === 0)
2910 continue;
2911
2912 let p = {};
2913 p.n = this.data[di].bin[i].count;
2914 p.x = Math.round(this.timeToX(this.data[di].time[i]));
2915 p.t = this.data[di].time[i];
2916
2917 p.first = this.valueToY(this.data[di].bin[i].firstValue);
2918 p.min = this.valueToY(this.data[di].bin[i].minValue);
2919 p.minValue = this.data[di].bin[i].minValue;
2920 p.max = this.valueToY(this.data[di].bin[i].maxValue);
2921 p.maxValue = this.data[di].bin[i].maxValue;
2922 p.last = this.valueToY(this.data[di].bin[i].lastValue);
2923
2924 if (this.data[di].binRaw) {
2925 p.rawFirstValue = this.data[di].binRaw[i].firstValue;
2926 p.rawMinValue = this.data[di].binRaw[i].minValue;
2927 p.rawMaxValue = this.data[di].binRaw[i].maxValue;
2928 p.rawLastValue = this.data[di].binRaw[i].lastValue;
2929 }
2930
2931 this.p[di][n] = p;
2932
2933 this.x[di][n] = p.x;
2934
2935 n++;
2936 }
2937
2938 // truncate arrays if now shorter
2939 this.p[di].length = n;
2940 this.x[di].length = n;
2941 if (this.data[di].rawValue)
2942 this.vRaw[di].length = n;
2943 }
2944 }
2945 }
2946
2947 // draw shaded areas
2948 if (this.showFill) {
2949 for (let di = 0; di < this.data.length; di++) {
2950 if (this.solo.active && this.solo.index !== di)
2951 continue;
2952
2953 if (this.events[di] === "Run transitions")
2954 continue;
2955
2956 ctx.fillStyle = this.param["Colour"][di];
2957
2958 // don't draw lines over "gaps"
2959 let gap = this.timeToXScale * 600; // 10 min
2960 if (gap < 5)
2961 gap = 5; // minimum of 5 pixels
2962
2963 if (this.binned) {
2964 if (this.p[di].length > 0) {
2965 let p = this.p[di][0];
2966 let x0 = p.x;
2967 let xold = p.x;
2968 let y0 = p.first;
2969 ctx.beginPath();
2970 ctx.moveTo(p.x, p.first);
2971 ctx.lineTo(p.x, p.last);
2972 for (let i = 1; i < this.p[di].length; i++) {
2973 p = this.p[di][i];
2974 if (p.x - xold < gap) {
2975 ctx.lineTo(p.x, p.first);
2976 ctx.lineTo(p.x, p.last);
2977 } else {
2978 ctx.lineTo(xold, this.valueToY(0));
2979 ctx.lineTo(p.x, this.valueToY(0));
2980 ctx.lineTo(p.x, p.first);
2981 ctx.lineTo(p.x, p.last);
2982 }
2983 xold = p.x;
2984 }
2985 ctx.lineTo(xold, this.valueToY(0));
2986 ctx.lineTo(x0, this.valueToY(0));
2987 ctx.lineTo(x0, y0);
2988 ctx.globalAlpha = 0.1;
2989 ctx.fill();
2990 ctx.globalAlpha = 1;
2991 }
2992 } else { // binned
2993 if (this.x[di].length > 0 && this.y[di].length > 0) {
2994 let x = this.x[di][0];
2995 let y = this.y[di][0];
2996 let x0 = x;
2997 let y0 = y;
2998 let xold = x;
2999 ctx.beginPath();
3000 ctx.moveTo(x, y);
3001 for (let i = 1; i < this.x[di].length; i++) {
3002 x = this.x[di][i];
3003 y = this.y[di][i];
3004 if (x - xold < gap)
3005 ctx.lineTo(x, y);
3006 else {
3007 ctx.lineTo(xold, this.valueToY(0));
3008 ctx.lineTo(x, this.valueToY(0));
3009 ctx.lineTo(x, y);
3010 }
3011 xold = x;
3012 }
3013 ctx.lineTo(xold, this.valueToY(0));
3014 ctx.lineTo(x0, this.valueToY(0));
3015 ctx.lineTo(x0, y0);
3016 ctx.globalAlpha = 0.1;
3017 ctx.fill();
3018 ctx.globalAlpha = 1;
3019 }
3020 }
3021 }
3022 }
3023
3024 // profile("Draw shaded areas");
3025
3026 // draw graphs
3027 for (let di = 0; di < this.data.length; di++) {
3028 if (this.solo.active && this.solo.index !== di)
3029 continue;
3030
3031 if (this.events[di] === "Run transitions") {
3032
3033 if (this.tags[di] === "State") {
3034 if (this.x[di].length < 200) {
3035 for (let i = 0; i < this.x[di].length; i++) {
3036 if (this.v[di][i] === 1) {
3037 ctx.strokeStyle = "#FF0000";
3038 ctx.fillStyle = "#808080";
3039 ctx.textAlign = "right";
3040 ctx.textBaseline = "top";
3041 ctx.fillText(this.v[di + 1][i], this.x[di][i] - 5, this.y2 + 3);
3042 } else if (this.v[di][i] === 3) {
3043 ctx.strokeStyle = "#00A000";
3044 ctx.fillStyle = "#808080";
3045 ctx.textAlign = "left";
3046 ctx.textBaseline = "top";
3047 ctx.fillText(this.v[di + 1][i], this.x[di][i] + 3, this.y2 + 3);
3048 } else {
3049 ctx.strokeStyle = "#F9A600";
3050 }
3051
3052 ctx.setLineDash([8, 2]);
3053 ctx.drawLine(Math.floor(this.x[di][i]), this.y1, Math.floor(this.x[di][i]), this.y2);
3054 ctx.setLineDash([]);
3055 }
3056 }
3057 }
3058
3059 } else {
3060
3061 ctx.strokeStyle = this.param["Colour"][di];
3062
3063 // don't draw lines over "gaps"
3064 let gap = this.timeToXScale * 600; // 10 min
3065 if (gap < 5)
3066 gap = 5; // minimum of 5 pixels
3067
3068 if (this.binned) {
3069 if (this.p[di].length > 0) {
3070 let p = this.p[di][0];
3071 //console.log("di:" + di + " i:" + 0 + " x:" + p.x, " y:" + p.first);
3072 let xold = p.x;
3073 ctx.beginPath();
3074 ctx.moveTo(p.x, p.first);
3075 ctx.lineTo(p.x, p.max + 1); // in case min==max
3076 ctx.lineTo(p.x, p.min);
3077 ctx.lineTo(p.x, p.last);
3078 for (let i = 1; i < this.p[di].length; i++) {
3079 p = this.p[di][i];
3080 //console.log("di:" + di + " i:" + i + " x:" + p.x, " y:" + p.first);
3081 if (p.x - xold < gap) {
3082 // draw lines first - max - min - last
3083 ctx.lineTo(p.x, p.first);
3084 ctx.lineTo(p.x, p.max + 1); // in case min==max
3085 ctx.lineTo(p.x, p.min);
3086 ctx.lineTo(p.x, p.last);
3087 } else { // don't draw gap
3088 // draw lines first - max - min - last
3089 ctx.moveTo(p.x, p.first);
3090 ctx.lineTo(p.x, p.max + 1); // in case min==max
3091 ctx.lineTo(p.x, p.min);
3092 ctx.lineTo(p.x, p.last);
3093 }
3094 xold = p.x;
3095 }
3096 ctx.stroke();
3097 }
3098 } else { // binned
3099 if (this.x[di].length === 1) {
3100 let x = this.x[di][0];
3101 let y = this.y[di][0];
3102 ctx.fillStyle = this.param["Colour"][di];
3103 ctx.fillRect(x - 1, y - 1, 3, 3);
3104 } else {
3105 if (this.x[di].length > 0) {
3106 ctx.beginPath();
3107 let x = this.x[di][0];
3108 let y = this.y[di][0];
3109 let xold = x;
3110 ctx.moveTo(x, y);
3111 for (let i = 1; i < this.x[di].length; i++) {
3112 let x = this.x[di][i];
3113 let y = this.y[di][i];
3114 if (x - xold > gap)
3115 ctx.moveTo(x, y);
3116 else
3117 ctx.lineTo(x, y);
3118 xold = x;
3119 }
3120 ctx.stroke();
3121 }
3122 }
3123 }
3124 }
3125 }
3126
3127 ctx.restore(); // remove clipping
3128
3129 // profile("Draw graphs");
3130
3131 // labels with variable names and values
3132 if (this.showLabels) {
3133 if (this.solo.active)
3134 this.variablesHeight = 17 + 7;
3135 else
3136 this.variablesHeight = this.param["Variables"].length * 17 + 7;
3137 this.variablesWidth = 0;
3138
3139 // determine width of widest label
3140 this.param["Variables"].forEach((v, i) => {
3141 let width;
3142 if (this.param.Label[i] !== "") {
3143 width = ctx.measureText(this.param.Label[i]).width;
3144 } else {
3145 width = ctx.measureText(splitEventAndTagName(v)[1]).width;
3146 }
3147
3148 if (this.param["Show raw value"] !== undefined &&
3149 this.param["Show raw value"][i])
3150 width += ctx.measureText(" (Raw)").width;
3151
3152 width += 20; // space between name and value
3153
3154 if (this.v[i] !== undefined && this.v[i].length > 0) {
3155 // use last point in array
3156 let index = this.v[i].length - 1;
3157
3158 // use point at current marker
3159 if (this.marker.active)
3160 index = this.marker.index;
3161
3162 if (index < this.v[i].length) {
3163 let value;
3164 if (this.param["Show raw value"] !== undefined &&
3165 this.param["Show raw value"][i])
3166 value = this.vRaw[i][index];
3167 else
3168 value = this.v[i][index];
3169
3170 // convert value to string with 6 digits
3171 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3172 width += ctx.measureText(str).width;
3173 }
3174 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3175 // use last point in array
3176 let index = this.p[i].length - 1;
3177
3178 // use point at current marker
3179 if (this.marker.active)
3180 index = this.marker.index;
3181
3182 if (index < this.p[i].length) {
3183 let value;
3184 if (this.param["Show raw value"] !== undefined &&
3185 this.param["Show raw value"][i])
3186 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3187 else
3188 value = (this.p[i][index].minValue + this.p[i][index].maxValue)/2;
3189
3190 // convert value to string with 6 digits
3191 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3192 width += ctx.measureText(str).width;
3193 }
3194 } else {
3195 width += ctx.measureText(convertLastWritten(this.lastWritten[i])).width;
3196 }
3197
3198 this.variablesWidth = Math.max(this.variablesWidth, width);
3199 });
3200
3201 let xLabel = this.x1;
3202 if (this.solo.active)
3203 xLabel = this.x1 + 28;
3204
3205 ctx.save();
3206 ctx.beginPath();
3207 ctx.rect(xLabel, this.y2, 25 + this.variablesWidth + 7, this.variablesHeight + 2);
3208 ctx.clip();
3209
3210 ctx.strokeStyle = this.color.axis;
3211 ctx.fillStyle = "#F0F0F0";
3212 ctx.globalAlpha = 0.5;
3213 ctx.strokeRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3214 ctx.fillRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3215 ctx.globalAlpha = 1;
3216
3217 this.param["Variables"].forEach((v, i) => {
3218
3219 if (this.solo.active && i !== this.solo.index)
3220 return;
3221
3222 let yLabel = 0;
3223 if (!this.solo.active)
3224 yLabel = i * 17;
3225
3226 ctx.lineWidth = 4;
3227 ctx.strokeStyle = this.param["Colour"][i];
3228 ctx.drawLine(xLabel + 5, this.y2 + 14 + yLabel, xLabel + 20, this.y2 + 14 + yLabel);
3229 ctx.lineWidth = 1;
3230
3231 ctx.textAlign = "left";
3232 ctx.textBaseline = "middle";
3233 ctx.fillStyle = "#404040";
3234
3235 let str;
3236 if (this.param.Label[i] !== "")
3237 str = this.param.Label[i];
3238 else
3239 str = splitEventAndTagName(v)[1];
3240
3241 if (this.param["Show raw value"] !== undefined &&
3242 this.param["Show raw value"][i])
3243 str += " (Raw)";
3244
3245 ctx.fillText(str, xLabel + 25, this.y2 + 14 + yLabel);
3246
3247 ctx.textAlign = "right";
3248
3249 // un-binned data
3250 if (this.v[i] !== undefined && this.v[i].length > 0) {
3251 // use last point in array
3252 let index = this.v[i].length - 1;
3253
3254 // use point at current marker
3255 if (this.marker.active)
3256 index = this.marker.index;
3257
3258 if (index < this.v[i].length) {
3259 // convert value to string with 6 digits
3260 let value;
3261 if (this.param["Show raw value"] !== undefined &&
3262 this.param["Show raw value"][i])
3263 value = this.vRaw[i][index];
3264 else
3265 value = this.v[i][index];
3266 let str = value.toPrecision(this.yPrecision).stripZeros();
3267 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3268 }
3269 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3270
3271 // binned data
3272
3273 // use last point in array
3274 let index = this.p[i].length - 1;
3275
3276 // use point at current marker
3277 if (this.marker.active)
3278 index = this.marker.index;
3279
3280 if (index < this.p[i].length) {
3281 // convert value to string with 6 digits
3282 let value;
3283 if (this.param["Show raw value"] !== undefined &&
3284 this.param["Show raw value"][i])
3285 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3286 else
3287 value = (this.p[i][index].minValue + this.p[i][index].maxValue) / 2;
3288 let str = value.toPrecision(this.yPrecision).stripZeros();
3289 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3290 }
3291
3292 } else {
3293
3294 if (this.lastWritten.length > 0) {
3295 if (this.lastWritten[i] > this.tMax) {
3296 //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));
3297 update_last_written = true;
3298 }
3299 ctx.fillText(convertLastWritten(this.lastWritten[i]),
3300 xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3301 } else {
3302 //console.log("last_written was not loaded yet");
3303 update_last_written = true;
3304 }
3305 }
3306
3307 });
3308
3309 ctx.restore(); // remove clipping
3310 }
3311
3312 // "updating" notice
3313 if (this.pendingUpdates > 0) {
3314 let str = "Updating data ...";
3315 ctx.strokeStyle = "#404040";
3316 ctx.fillStyle = "#FFC0C0";
3317 ctx.fillRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3318 ctx.strokeRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3319 ctx.fillStyle = "#404040";
3320 ctx.textAlign = "left";
3321 ctx.textBaseline = "middle";
3322 ctx.fillText(str, this.x1 + 10, this.y1 - 13);
3323 }
3324
3325 let no_data = true;
3326
3327 for (let i = 0; i < this.data.length; i++) {
3328 if (this.data[i].time === undefined || this.data[i].time.length === 0) {
3329 } else {
3330 no_data = false;
3331 }
3332 }
3333
3334 // "empty window" notice
3335 if (no_data && this.pendingUpdates === 0) {
3336 ctx.font = "16px sans-serif";
3337 let str = "No data available";
3338 ctx.strokeStyle = "#404040";
3339 ctx.fillStyle = "#F0F0F0";
3340 let w = ctx.measureText(str).width + 10;
3341 let h = 16 + 10;
3342 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3343 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3344 ctx.fillStyle = "#404040";
3345 ctx.textAlign = "center";
3346 ctx.textBaseline = "middle";
3347 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
3348 ctx.font = "14px sans-serif";
3349 }
3350
3351 // buttons
3352 if (this.showMenuButtons) {
3353 let y = 0;
3354 let buttonSize = 20;
3355 this.button.forEach(b => {
3356 b.x1 = this.width - buttonSize - 6;
3357 b.y1 = 6 + y * (buttonSize + 4);
3358 b.width = buttonSize + 4;
3359 b.height = buttonSize + 4;
3360 b.enabled = true;
3361
3362 if (b.src === "maximize-2.svg") {
3363 let s = window.location.href;
3364 if (s.indexOf("&A") > -1)
3365 s = s.substr(0, s.indexOf("&A"));
3366 if (s === encodeURI(this.baseURL + "&group=" + this.group + "&panel=" + this.panel)) {
3367 b.enabled = false;
3368 return;
3369 }
3370 }
3371
3372 if (b.src === "corner-down-left.svg") {
3373 b.x1 = this.x1;
3374 b.y1 = this.y2;
3375 if (this.solo.active)
3376 b.enabled = true;
3377 else {
3378 b.enabled = false;
3379 return;
3380 }
3381 }
3382
3383 if (b.src === "play.svg" && !this.scroll)
3384 ctx.fillStyle = "#FFC0C0";
3385 else
3386 ctx.fillStyle = "#F0F0F0";
3387 ctx.strokeStyle = "#808080";
3388 ctx.fillRect(b.x1, b.y1, b.width, b.height);
3389 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
3390 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
3391
3392 y++;
3393 });
3394 }
3395
3396 // zoom buttons
3397 if (this.showZoomButtons) {
3398 let xb;
3399 if (this.showMenuButtons)
3400 xb = this.width - 26 - 40;
3401 else
3402 xb = this.width - 41;
3403 let yb = this.y1 - 20;
3404 ctx.fillStyle = "#F0F0F0";
3405 ctx.globalAlpha = 0.5;
3406 ctx.fillRect(xb, yb, 20, 20);
3407 ctx.globalAlpha = 1;
3408 ctx.strokeStyle = "#808080";
3409 ctx.strokeRect(xb, yb, 20, 20);
3410 ctx.strokeStyle = "#202020";
3411 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3412 ctx.drawLine(xb + 10, yb + 4, xb + 10, yb + 17);
3413
3414 xb += 20;
3415 ctx.globalAlpha = 0.5;
3416 ctx.fillRect(xb, yb, 20, 20);
3417 ctx.globalAlpha = 1;
3418 ctx.strokeStyle = "#808080";
3419 ctx.strokeRect(xb, yb, 20, 20);
3420 ctx.strokeStyle = "#202020";
3421 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3422 }
3423
3424 // axis zoom
3425 if (this.zoom.x.active) {
3426 ctx.fillStyle = "#808080";
3427 ctx.globalAlpha = 0.2;
3428 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
3429 ctx.globalAlpha = 1;
3430 ctx.strokeStyle = "#808080";
3431 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
3432 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
3433 }
3434 if (this.zoom.y.active) {
3435 ctx.fillStyle = "#808080";
3436 ctx.globalAlpha = 0.2;
3437 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
3438 ctx.globalAlpha = 1;
3439 ctx.strokeStyle = "#808080";
3440 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
3441 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
3442 }
3443
3444 // marker
3445 if (this.marker.active) {
3446
3447 // round marker
3448 ctx.beginPath();
3449 ctx.globalAlpha = 0.1;
3450 ctx.arc(this.marker.x, this.marker.y, 10, 0, 2 * Math.PI);
3451 ctx.fillStyle = "#000000";
3452 ctx.fill();
3453 ctx.globalAlpha = 1;
3454
3455 ctx.beginPath();
3456 ctx.arc(this.marker.x, this.marker.y, 4, 0, 2 * Math.PI);
3457 ctx.fillStyle = "#000000";
3458 ctx.fill();
3459
3460 ctx.strokeStyle = "#A0A0A0";
3461 ctx.drawLine(this.marker.x, this.y1, this.marker.x, this.y2);
3462
3463 // text label
3464 let v = this.marker.v;
3465
3466 let s;
3467 if (this.param.Label[this.marker.graphIndex] !== "")
3468 s = this.param.Label[this.marker.graphIndex];
3469 else
3470 s = this.param["Variables"][this.marker.graphIndex];
3471
3472 if (this.param["Show raw value"] !== undefined &&
3473 this.param["Show raw value"][this.marker.graphIndex])
3474 s += " (Raw)";
3475
3476 s += ": " + v.toPrecision(this.yPrecision).stripZeros();
3477
3478 let w = ctx.measureText(s).width + 6;
3479 let h = ctx.measureText("M").width * 1.2 + 6;
3480 let x = this.marker.mx + 20;
3481 let y = this.marker.my + h / 3 * 2;
3482 let xl = x;
3483 let yl = y;
3484
3485 if (x + w >= this.x2) {
3486 x = this.marker.x - 20 - w;
3487 xl = x + w;
3488 }
3489
3490 if (y > (this.y1 - this.y2) / 2) {
3491 y = this.marker.y - h / 3 * 5;
3492 yl = y + h;
3493 }
3494
3495 ctx.strokeStyle = "#808080";
3496 ctx.fillStyle = "#F0F0F0";
3497 ctx.textBaseline = "middle";
3498 ctx.fillRect(x, y, w, h);
3499 ctx.strokeRect(x, y, w, h);
3500 ctx.fillStyle = "#404040";
3501 ctx.fillText(s, x + 3, y + h / 2);
3502
3503 // vertical line
3504 ctx.strokeStyle = "#808080";
3505 ctx.drawLine(this.marker.x, this.marker.y, xl, yl);
3506
3507 // time label
3508 s = timeToLabel(this.marker.t, 1, true);
3509 w = ctx.measureText(s).width + 10;
3510 h = ctx.measureText("M").width * 1.2 + 11;
3511 x = this.marker.x - w / 2;
3512 y = this.y1;
3513 if (x <= this.x1)
3514 x = this.x1;
3515 if (x + w >= this.x2)
3516 x = this.x2 - w;
3517
3518 ctx.strokeStyle = "#808080";
3519 ctx.fillStyle = "#F0F0F0";
3520 ctx.fillRect(x, y, w, h);
3521 ctx.strokeRect(x, y, w, h);
3522 ctx.fillStyle = "#404040";
3523 ctx.fillText(s, x + 5, y + h / 2);
3524 }
3525
3526 this.lastDrawTime = new Date().getTime();
3527
3528 // profile("Finished draw");
3529
3530 if (update_last_written) {
3531 this.updateLastWritten();
3532 }
3533
3534 // update URL
3535 if (this.updateURLTimer !== undefined)
3536 window.clearTimeout(this.updateURLTimer);
3537
3538 if (this.plotIndex === 0 && this.floating !== true)
3539 this.updateURLTimer = window.setTimeout(this.updateURL.bind(this), 10);
3540};
3541
3542MhistoryGraph.prototype.drawVAxis = function (ctx, x1, y1, height, minor, major,
3543 text, label, grid, ymin, ymax, logaxis, draw) {
3544 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
3545 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
3546 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
3547
3548 if (x1 > 0)
3549 ctx.textAlign = "right";
3550 else
3551 ctx.textAlign = "left";
3552 ctx.textBaseline = "middle";
3553 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
3554
3555 if (ymax <= ymin || height <= 0)
3556 return undefined;
3557
3558 if (!isFinite(ymax - ymin) || ymax == Number.MAX_VALUE) {
3559 dy = Number.MAX_VALUE / 10;
3560 label_dy = dy;
3561 major_dy = dy;
3562 n_sig1 = 1;
3563 } else if (logaxis) {
3564 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
3565 if (dy === 0) {
3566 ymin = 1E-20;
3567 dy = 1E-20;
3568 }
3569 label_dy = dy;
3570 major_dy = dy * 10;
3571 n_sig1 = 4;
3572 } else {
3573 // use 6 as min tick distance
3574 dy = (ymax - ymin) / (height / 6);
3575
3576 int_dy = Math.floor(Math.log(dy) / Math.log(10));
3577 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
3578
3579 if (frac_dy < 0) {
3580 frac_dy += 1;
3581 int_dy -= 1;
3582 }
3583
3584 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
3585 major_base = label_base = tick_base + 1;
3586
3587 // rounding up of dy, label_dy
3588 dy = Math.pow(10, int_dy) * base[tick_base];
3589 major_dy = Math.pow(10, int_dy) * base[major_base];
3590 label_dy = major_dy;
3591
3592 // number of significant digits
3593 if (ymin === 0)
3594 n_sig1 = 1;
3595 else
3596 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
3597 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3598
3599 if (ymax === 0)
3600 n_sig2 = 1;
3601 else
3602 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
3603 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3604
3605 n_sig1 = Math.max(n_sig1, n_sig2);
3606 n_sig1 = Math.max(1, n_sig1);
3607
3608 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
3609 if (Math.abs(ymin) < 100000)
3610 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
3611 Math.log(10) + 0.001) + 1);
3612 if (Math.abs(ymax) < 100000)
3613 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
3614 Math.log(10) + 0.001) + 1);
3615
3616 // increase label_dy if labels would overlap
3617 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
3618 label_base++;
3619 label_dy = Math.pow(10, int_dy) * base[label_base];
3620 if (label_base % 3 === 2 && major_base % 3 === 1) {
3621 major_base++;
3622 major_dy = Math.pow(10, int_dy) * base[major_base];
3623 }
3624 }
3625 }
3626
3627 y_act = Math.floor(ymin / dy) * dy;
3628
3629 let last_label_y = y1;
3630 let maxwidth = 0;
3631
3632 if (draw) {
3633 ctx.strokeStyle = this.color.axis;
3634 ctx.drawLine(x1, y1, x1, y1 - height);
3635 }
3636
3637 do {
3638 if (logaxis)
3639 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
3640 (Math.log(ymax) - Math.log(ymin)) * height;
3641 else if (!(isFinite(ymax - ymin)))
3642 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
3643 else
3644 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
3645 ys = Math.round(y_screen);
3646
3647 if (y_screen < y1 - height - 0.001 || isNaN(ys))
3648 break;
3649
3650 if (y_screen <= y1 + 0.001) {
3651 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
3652 dy / major_dy / 10.0) {
3653
3654 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
3655 dy / label_dy / 10.0) {
3656 // label tick mark
3657 if (draw) {
3658 ctx.strokeStyle = this.color.axis;
3659 ctx.drawLine(x1, ys, x1 + text, ys);
3660 }
3661
3662 // grid line
3663 if (grid !== 0 && ys < y1 && ys > y1 - height)
3664 if (draw) {
3665 ctx.strokeStyle = this.color.grid;
3666 ctx.drawLine(x1, ys, x1 + grid, ys);
3667 }
3668
3669 // label
3670 if (label !== 0) {
3671 let str;
3672 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3673 str = y_act.toExponential(n_sig1).stripZeros();
3674 else
3675 str = y_act.toPrecision(n_sig1).stripZeros();
3676 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3677 if (draw) {
3678 ctx.strokeStyle = this.color.label;
3679 ctx.fillStyle = this.color.label;
3680 ctx.fillText(str, x1 + label, ys);
3681 }
3682 last_label_y = ys - textHeight / 2;
3683 }
3684 } else {
3685 // major tick mark
3686 if (draw) {
3687 ctx.strokeStyle = this.color.axis;
3688 ctx.drawLine(x1, ys, x1 + major, ys);
3689 }
3690
3691 // grid line
3692 if (grid !== 0 && ys < y1 && ys > y1 - height)
3693 if (draw) {
3694 ctx.strokeStyle = this.color.grid;
3695 ctx.drawLine(x1, ys, x1 + grid, ys);
3696 }
3697 }
3698
3699 if (logaxis) {
3700 dy *= 10;
3701 major_dy *= 10;
3702 label_dy *= 10;
3703 }
3704
3705 } else
3706 // minor tick mark
3707 if (draw) {
3708 ctx.strokeStyle = this.color.axis;
3709 ctx.drawLine(x1, ys, x1 + minor, ys);
3710 }
3711
3712 // for logaxis, also put labels on minor tick marks
3713 if (logaxis) {
3714 if (label !== 0) {
3715 let str;
3716 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3717 str = y_act.toExponential(n_sig1).stripZeros();
3718 else
3719 str = y_act.toPrecision(n_sig1).stripZeros();
3720 if (ys - textHeight / 2 > y1 - height &&
3721 ys + textHeight / 2 < y1 &&
3722 ys + textHeight < last_label_y + 2) {
3723 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3724 if (draw) {
3725 ctx.strokeStyle = this.color.label;
3726 ctx.fillStyle = this.color.label;
3727 ctx.fillText(str, x1 + label, ys);
3728 }
3729 }
3730
3731 last_label_y = ys;
3732 }
3733 }
3734 }
3735
3736 y_act += dy;
3737
3738 // suppress 1.23E-17 ...
3739 if (Math.abs(y_act) < dy / 100)
3740 y_act = 0;
3741
3742 } while (1);
3743
3744 return maxwidth;
3745};
3746
3747let options1 = {
3748 timeZone: 'UTC',
3749 day: '2-digit', month: 'short', year: '2-digit',
3750 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3751};
3752
3753let options2 = {
3754 timeZone: 'UTC',
3755 day: '2-digit', month: 'short', year: '2-digit',
3756 hour12: false, hour: '2-digit', minute: '2-digit'
3757};
3758
3759let options3 = {
3760 timeZone: 'UTC',
3761 day: '2-digit', month: 'short', year: '2-digit',
3762 hour12: false, hour: '2-digit', minute: '2-digit'
3763};
3764
3765let options4 = {
3766 timeZone: 'UTC',
3767 day: '2-digit', month: 'short', year: '2-digit'
3768};
3769
3770let options5 = {
3771 timeZone: 'UTC',
3772 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3773};
3774
3775let options6 = {
3776 timeZone: 'UTC',
3777 hour12: false, hour: '2-digit', minute: '2-digit'
3778};
3779
3780let options7 = {
3781 timeZone: 'UTC',
3782 hour12: false, hour: '2-digit', minute: '2-digit'
3783};
3784
3785let options8 = {
3786 timeZone: 'UTC',
3787 day: '2-digit', month: 'short', year: '2-digit',
3788 hour12: false, hour: '2-digit', minute: '2-digit'
3789};
3790
3791let options9 = {
3792 timeZone: 'UTC',
3793 day: '2-digit', month: 'short', year: '2-digit'
3794};
3795
3796function timeToLabel(sec, base, forceDate) {
3797 let d = mhttpd_get_display_time(sec).date;
3798
3799 if (forceDate) {
3800 if (base < 60) {
3801 return d.toLocaleTimeString('en-GB', options1);
3802 } else if (base < 600) {
3803 return d.toLocaleTimeString('en-GB', options2);
3804 } else if (base < 3600 * 24) {
3805 return d.toLocaleTimeString('en-GB', options3);
3806 } else {
3807 return d.toLocaleDateString('en-GB', options4);
3808 }
3809 }
3810
3811 if (base < 60) {
3812 return d.toLocaleTimeString('en-GB', options5);
3813 } else if (base < 600) {
3814 return d.toLocaleTimeString('en-GB', options6);
3815 } else if (base < 3600 * 3) {
3816 return d.toLocaleTimeString('en-GB', options7);
3817 } else if (base < 3600 * 24) {
3818 return d.toLocaleTimeString('en-GB', options8);
3819 } else {
3820 return d.toLocaleDateString('en-GB', options9);
3821 }
3822}
3823
3824MhistoryGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
3825 text, label, grid, xmin, xmax) {
3826 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
3827 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
3828
3829 ctx.textAlign = "left";
3830 ctx.textBaseline = "top";
3831
3832 if (xmax <= xmin || width <= 0)
3833 return;
3834
3835 /* force date display if xmax not today */
3836 let d1 = new Date(xmax * 1000);
3837 let d2 = new Date();
3838 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
3839
3840 /* use 5 pixel as min tick distance */
3841 let dx = Math.round((xmax - xmin) / (width / 5));
3842
3843 let tick_base;
3844 for (tick_base = 0; base[tick_base]; tick_base++) {
3845 if (base[tick_base] > dx)
3846 break;
3847 }
3848 if (!base[tick_base])
3849 tick_base--;
3850 dx = base[tick_base];
3851
3852 let major_base = tick_base;
3853 let major_dx = dx;
3854
3855 let label_base = major_base;
3856 let label_dx = dx;
3857
3858 do {
3859 let str = timeToLabel(xmin, label_dx, forceDate);
3860 let maxwidth = ctx.measureText(str).width;
3861
3862 /* increasing label_dx, if labels would overlap */
3863 if (maxwidth > 0.75 * label_dx / (xmax - xmin) * width) {
3864 if (base[label_base + 1])
3865 label_dx = base[++label_base];
3866 else
3867 label_dx += 3600 * 24;
3868
3869 if (label_base > major_base + 1 || !base[label_base + 1]) {
3870 if (base[major_base + 1])
3871 major_dx = base[++major_base];
3872 else
3873 major_dx += 3600 * 24;
3874 }
3875
3876 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3877 if (base[tick_base + 1])
3878 dx = base[++tick_base];
3879 else
3880 dx += 3600 * 24;
3881 }
3882
3883 } else
3884 break;
3885 } while (1);
3886
3887 let d = new Date(xmin * 1000);
3888 let tz = d.getTimezoneOffset() * 60;
3889
3890 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3891
3892 ctx.strokeStyle = this.color.axis;
3893 ctx.drawLine(x1, y1, x1 + width, y1);
3894
3895 do {
3896 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3897
3898 if (xs > x1 + width + 0.001)
3899 break;
3900
3901 if (xs >= x1) {
3902 if ((x_act - tz) % major_dx === 0) {
3903 if ((x_act - tz) % label_dx === 0) {
3904 // label tick mark
3905 ctx.strokeStyle = this.color.axis;
3906 ctx.drawLine(xs, y1, xs, y1 + text);
3907
3908 // grid line
3909 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3910 ctx.strokeStyle = this.color.grid;
3911 ctx.drawLine(xs, y1, xs, y1 + grid);
3912 }
3913
3914 // label
3915 if (label !== 0) {
3916 let str = timeToLabel(x_act, label_dx, forceDate);
3917
3918 // if labels at edge, shift them in
3919 let xl = xs - ctx.measureText(str).width / 2;
3920 if (xl < 0)
3921 xl = 0;
3922 if (xl + ctx.measureText(str).width >= xr)
3923 xl = xr - ctx.measureText(str).width - 1;
3924 ctx.strokeStyle = this.color.label;
3925 ctx.fillStyle = this.color.label;
3926 ctx.fillText(str, xl, y1 + label);
3927 }
3928 } else {
3929 // major tick mark
3930 ctx.strokeStyle = this.color.axis;
3931 ctx.drawLine(xs, y1, xs, y1 + major);
3932 }
3933
3934 // grid line
3935 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3936 ctx.strokeStyle = this.color.grid;
3937 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
3938 }
3939 } else {
3940 // minor tick mark
3941 ctx.strokeStyle = this.color.axis;
3942 ctx.drawLine(xs, y1, xs, y1 + minor);
3943 }
3944 }
3945
3946 x_act += dx;
3947
3948 } while (1);
3949};
3950
3951MhistoryGraph.prototype.download = function (mode) {
3952
3953 let leftDate = mhttpd_get_display_time(this.tMin).date;
3954 let rightDate = mhttpd_get_display_time(this.tMax).date;
3955 let filename = this.group + "-" + this.panel + "-" +
3956 leftDate.getFullYear() +
3957 ("0" + (leftDate.getUTCMonth() + 1)).slice(-2) +
3958 ("0" + leftDate.getUTCDate()).slice(-2) + "-" +
3959 ("0" + leftDate.getUTCHours()).slice(-2) +
3960 ("0" + leftDate.getUTCMinutes()).slice(-2) +
3961 ("0" + leftDate.getUTCSeconds()).slice(-2) + "-" +
3962 rightDate.getFullYear() +
3963 ("0" + (rightDate.getUTCMonth() + 1)).slice(-2) +
3964 ("0" + rightDate.getUTCDate()).slice(-2) + "-" +
3965 ("0" + rightDate.getUTCHours()).slice(-2) +
3966 ("0" + rightDate.getUTCMinutes()).slice(-2) +
3967 ("0" + rightDate.getUTCSeconds()).slice(-2);
3968
3969 // use trick from FileSaver.js
3970 let a = document.getElementById('downloadHook');
3971 if (a === null) {
3972 a = document.createElement("a");
3973 a.style.display = "none";
3974 a.id = "downloadHook";
3975 document.body.appendChild(a);
3976 }
3977
3978 if (mode === "CSV") {
3979 filename += ".csv";
3980
3981 let data = "";
3982 this.param["Variables"].forEach(v => {
3983 data += "Time,";
3984 if (this.binned)
3985 data += v + " MIN," + v + " MAX,";
3986 else
3987 data += v + ",";
3988 });
3989 data = data.slice(0, -1);
3990 data += '\n';
3991
3992 let maxlen = 0;
3993 let nvar = this.param["Variables"].length;
3994 for (let index=0 ; index < nvar ; index++)
3995 if (this.data[index].time.length > maxlen)
3996 maxlen = this.data[index].time.length;
3997 let index = [];
3998 for (let di=0 ; di < nvar ; di++)
3999 for (let i = 0; i < maxlen; i++) {
4000 if (i < this.data[di].time.length &&
4001 this.data[di].time[i] > this.tMin) {
4002 index[di] = i;
4003 break;
4004 }
4005 }
4006
4007 for (let i = 0; i < maxlen; i++) {
4008 let l = "";
4009 for (let di = 0 ; di < nvar ; di++) {
4010 if (index[di] < this.data[di].time.length &&
4011 this.data[di].time[index[di]] > this.tMin && this.data[di].time[index[di]] < this.tMax) {
4012 if (this.binned) {
4013 l += this.data[di].time[index[di]] + ",";
4014
4015 if (this.param["Show raw value"] !== undefined &&
4016 this.param["Show raw value"][di]) {
4017 l += this.data[di].binRaw[index[di]].minValue + ",";
4018 l += this.data[di].binRaw[index[di]].maxValue + ",";
4019 } else {
4020 l += this.data[di].bin[index[di]].minValue + ",";
4021 l += this.data[di].bin[index[di]].maxValue + ",";
4022 }
4023
4024 } else {
4025
4026 l += this.data[di].time[index[di]] + ",";
4027
4028 if (this.param["Show raw value"] !== undefined &&
4029 this.param["Show raw value"][di])
4030 l += this.data[di].rawValue[index[di]] + ",";
4031 else
4032 l += this.data[di].value[index[di]] + ",";
4033 }
4034 } else {
4035 l += ",,";
4036 }
4037 index[di]++;
4038 }
4039 if (l.split(',').some(s => s)) { // don't add if only commas
4040 l = l.slice(0, -1); // remove last comma
4041 data += l + '\n';
4042 }
4043 }
4044
4045 let blob = new Blob([data], {type: "text/csv"});
4046 let url = window.URL.createObjectURL(blob);
4047
4048 a.href = url;
4049 a.download = filename;
4050 a.click();
4051 window.URL.revokeObjectURL(url);
4052 dlgAlert("Data downloaded to '" + filename + "'");
4053
4054 } else if (mode === "PNG") {
4055 filename += ".png";
4056
4057 this.showZoomButtons = false;
4058 this.showMenuButtons = false;
4059 this.forceRedraw = true;
4060 this.forceConvert = true;
4061 this.draw();
4062
4063 let h = this;
4064 this.canvas.toBlob(function (blob) {
4065 let url = window.URL.createObjectURL(blob);
4066
4067 a.href = url;
4068 a.download = filename;
4069 a.click();
4070 window.URL.revokeObjectURL(url);
4071 dlgAlert("Image downloaded to '" + filename + "'");
4072
4073 h.showZoomButtons = true;
4074 h.showMenuButtons = true;
4075 h.forceRedraw = true;
4076 h.forceConvert = true;
4077 h.draw();
4078
4079 }, 'image/png');
4080 } else if (mode === "URL") {
4081 // Create new element
4082 let el = document.createElement('textarea');
4083
4084 // Set value (string to be copied)
4085 let url = this.baseURL + "&group=" + this.group + "&panel=" + this.panel +
4086 "&A=" + this.tMin + "&B=" + this.tMax;
4087 url = encodeURI(url);
4088 el.value = url;
4089
4090 // Set non-editable to avoid focus and move outside of view
4091 el.setAttribute('readonly', '');
4092 el.style = {position: 'absolute', left: '-9999px'};
4093 document.body.appendChild(el);
4094 // Select text inside element
4095 el.select();
4096 // Copy text to clipboard
4097 document.execCommand('copy');
4098 // Remove temporary element
4099 document.body.removeChild(el);
4100
4101 dlgMessage("Info", "URL<br/><br/>" + url + "<br/><br/>copied to clipboard", true, false);
4102 }
4103
4104};