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