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.contentEditable === true) return;
503 if (e.key === "u") { // 'u' key
504 // force next update t.Min-dt/2 to current time
505 let dt = Math.floor(this.tMax - this.tMin);
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[0] >= this.data[index].time[this.data[index].time.length-1]) {
1313
1314 // remove double event
1315 while (t1[0] === this.data[index].time[this.data[index].time.length-1]) {
1316 t1 = t1.slice(1);
1317 v1 = v1.slice(1);
1318 if (v1Raw.length > 0)
1319 v1Raw = v1Raw.slice(1);
1320 }
1321
1322 // add data to the right
1323 this.data[index].time = this.data[index].time.concat(t1);
1324 this.data[index].value = this.data[index].value.concat(v1);
1325 if (v1Raw.length > 0)
1326 this.data[index].rawValue = this.data[index].rawValue.concat(v1Raw);
1327
1328 } else if (t1[t1.length-1] < this.data[index].time[0]) {
1329
1330 // add data to the left
1331 this.data[index].time = t1.concat(this.data[index].time);
1332 this.data[index].value = v1.concat(this.data[index].value);
1333 if (v1Raw.length > 0)
1334 this.data[index].rawValue = v1Raw.concat(this.data[index].rawValue);
1335 }
1336
1337 if (index === 0) {
1338 for (let i = 1; i < this.data[index].time.length; i++)
1339 if (this.data[index].time[i] < this.data[index].time[i - 1]) {
1340 console.log("Error non-continuous data");
1341 log_hs_read("told", told[0], told[told.length-1]);
1342 log_hs_read("t1", t1[0], t1[t1.length-1]);
1343 }
1344 }
1345 }
1346 }
1347
1348 return true;
1349}
1350
1351MhistoryGraph.prototype.receiveDataBinned = function (rpc) {
1352
1353 // decode binary array
1354 let array = new Float64Array(rpc);
1355
1356 // let status = array[0];
1357 // let startTime = array[1];
1358 // let endTime = array[2];
1359 let numBins = array[3];
1360 let nVars = array[4];
1361
1362 let i = 5;
1363 // let hsStatus = array.slice(i, i + nVars);
1364 i += nVars;
1365 let numEntries = array.slice(i, i + nVars);
1366 i += nVars;
1367 // let lastTime = array.slice(i, i + nVars);
1368 i += nVars;
1369 // let lastValue = array.slice(i, i + nVars);
1370 i += nVars;
1371
1372 if (i >= array.length) {
1373 // RPC did not return any data
1374
1375 if (this.data === undefined) {
1376 // must initialize the arrays otherwise nothing works
1377 this.data = [];
1378 for (let index = 0; index < nVars; index++) {
1379 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1380 }
1381 }
1382
1383 return false;
1384 }
1385
1386 // push empty arrays on the first time
1387 if (this.data === undefined) {
1388 this.data = [];
1389 for (let index = 0; index < nVars; index++) {
1390 this.data.push({time:[], value:[], rawValue:[], bin:[], rawBin:[]});
1391 }
1392 }
1393
1394 let binSize = 0;
1395 let binSizeN = 0;
1396
1397 // create arrays of new values
1398 for (let index = 0; index < nVars; index++) {
1399 if (numEntries[index] === 0)
1400 continue;
1401
1402 let t1 = [];
1403 let bin1 = [];
1404 let binRaw1 = [];
1405
1406 // add data to the right
1407 let formula = this.param["Formula"];
1408 if (Array.isArray(formula))
1409 formula = formula[index];
1410
1411 if (formula === undefined || formula === "") {
1412 for (let j = 0; j < numBins; j++) {
1413
1414 let count = array[i++];
1415 // let mean = array[i++];
1416 // let rms = array[i++];
1417 i += 2;
1418 let minValue = array[i++];
1419 let maxValue = array[i++];
1420 let firstTime = array[i++];
1421 let firstValue = array[i++];
1422 let lastTime = array[i++];
1423 let lastValue = array[i++];
1424 let t = Math.floor((firstTime + lastTime) / 2);
1425
1426 if (count > 0) {
1427 // append to the right
1428 t1.push(t);
1429
1430 let bin = {};
1431 bin.count = count;
1432 bin.firstValue = firstValue;
1433 bin.lastValue = lastValue;
1434 bin.minValue = minValue;
1435 bin.maxValue = maxValue;
1436
1437 bin1.push(bin);
1438
1439 // calculate average bin count
1440 binSize += count;
1441 binSizeN++;
1442 }
1443 }
1444
1445 } else { // use formula
1446
1447 for (let j = 0; j < numBins; j++) {
1448
1449 let count = array[i++];
1450 // let mean = array[i++];
1451 // let rms = array[i++];
1452 i += 2;
1453 let minValue = array[i++];
1454 let maxValue = array[i++];
1455 let firstTime = array[i++];
1456 let firstValue = array[i++];
1457 let lastTime = array[i++];
1458 let lastValue = array[i++];
1459
1460 if (count > 0) {
1461 // append to the right
1462 t1.push(Math.floor((firstTime + lastTime) / 2));
1463
1464 let bin = {};
1465 let binRaw = {};
1466 let x = firstValue;
1467 binRaw.firstValue = firstValue;
1468 bin.firstValue = eval(formula);
1469 x = lastValue;
1470 binRaw.lastValue = lastValue;
1471 bin.lastValue = eval(formula);
1472 x = minValue;
1473 binRaw.minValue = minValue;
1474 bin.minValue = eval(formula);
1475 x = maxValue;
1476 binRaw.maxValue = maxValue;
1477 bin.maxValue = eval(formula);
1478
1479 bin1.push(bin);
1480 binRaw1.push(binRaw);
1481
1482 // calculate average bin count
1483 binSize += count;
1484 binSizeN++;
1485 }
1486 }
1487 }
1488
1489 if (t1.length > 0) {
1490
1491 let da = new Date(t1[0]*1000);
1492 let db = new Date(t1[t1.length-1]*1000);
1493
1494 if (index === 0)
1495 log_hs_read("receiveData binned", t1[0], t1[t1.length-1]);
1496
1497 if (this.data[index].time.length === 0 ||
1498 t1[0] > this.data[index].time[0]) {
1499
1500 // append to right if new data
1501 this.data[index].time = this.data[index].time.concat(t1);
1502 this.data[index].bin = this.data[index].bin.concat(bin1);
1503 if (binRaw1.length > 0)
1504 this.data[index].binRaw = this.data[index].rawValue.concat(binRaw1);
1505
1506 } else {
1507
1508 // append to left if old data
1509 this.data[index].time = t1.concat(this.data[index].time);
1510 this.data[index].bin = bin1.concat(this.data[index].bin);
1511 if (binRaw1.length > 0)
1512 this.data[index].binRaw = binRaw1.concat(this.data[index].binRaw);
1513 }
1514 }
1515 }
1516
1517 // calculate average bin size
1518 if (binSizeN > 0)
1519 this.binSize = binSize / binSizeN;
1520
1521 return true;
1522};
1523
1524MhistoryGraph.prototype.loadNewData = function () {
1525
1526 if (this.updateTimer)
1527 window.clearTimeout(this.updateTimer);
1528
1529 // don't update window if content is hidden (other tab, minimized, etc.)
1530 if (document.hidden) {
1531 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1532 return;
1533 }
1534
1535 // don't update if not in scrolling mode
1536 if (!this.scroll) {
1537 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 500);
1538 return;
1539 }
1540
1541 // update data from last point to current time
1542 let t1 = this.tMaxReceived;
1543 if (t1 === undefined)
1544 t1 = this.tMin;
1545 let t2 = Math.floor(new Date() / 1000);
1546
1547 // for strip-chart mode always use non-binned data
1548 this.binned = false;
1549
1550 log_hs_read("loadNewData un-binned", t1, t2);
1551 mjsonrpc_call("hs_read_arraybuffer",
1552 {
1553 "start_time": Math.floor(t1),
1554 "end_time": Math.floor(t2),
1555 "events": this.events,
1556 "tags": this.tags,
1557 "index": this.index
1558 }, "arraybuffer")
1559 .then(function (rpc) {
1560
1561 if (this.tMinRequested === undefined || t1 < this.tMinRequested) {
1562 this.tMinRequested = t1;
1563 this.tMinReceived = t1;
1564 }
1565
1566 if (this.tMaxRequested === undefined || t2 > this.tMaxRequested) {
1567 this.tMaxReceived = t2;
1568 this.tMaxRequested = t2;
1569 }
1570
1571 if (this.receiveData(rpc)) {
1572 this.findMinMax();
1573 this.scrollRedraw();
1574 }
1575
1576 this.updateTimer = window.setTimeout(this.loadNewData.bind(this), 1000);
1577
1578 }.bind(this)).catch(function (error) {
1579 mjsonrpc_error_alert(error);
1580 });
1581}
1582
1583MhistoryGraph.prototype.scrollRedraw = function () {
1584 if (this.scrollTimer)
1585 window.clearTimeout(this.scrollTimer);
1586
1587 if (this.scroll) {
1588 let dt = this.tMax - this.tMin;
1589 this.tMax = new Date() / 1000;
1590 this.tMin = this.tMax - dt;
1591 this.findMinMax();
1592 this.redraw(true);
1593
1594 // calculate time for one pixel
1595 dt = (this.tMax - this.tMin) / (this.x2 - this.x1);
1596 dt = Math.min(Math.max(0.1, dt), 60);
1597 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), dt / 2 * 1000);
1598 } else {
1599 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
1600 this.redraw(true);
1601 }
1602}
1603
1604MhistoryGraph.prototype.discardCurrentData = function () {
1605
1606 if (this.data === undefined)
1607 return;
1608
1609 // drop all data
1610 for (let i = 0 ; i<this.data.length ; i++) {
1611 this.data[i].time.length = 0;
1612 if (this.data[i].value)
1613 this.data[i].value.length = 0;
1614 if (this.data[i].bin)
1615 this.data[i].bin.length = 0;
1616 if (this.data[i].rawBin)
1617 this.data[i].rawBin.length = 0;
1618 if (this.data[i].rawValue)
1619 this.data[i].rawValue.length = 0;
1620
1621 this.x[i].length = 0;
1622 this.y[i].length = 0;
1623 this.t[i].length = 0;
1624 this.v[i].length = 0;
1625 if (this.vRaw[i])
1626 this.vRaw[i].length = 0;
1627 }
1628
1629 this.tMaxReceived = undefined ;
1630 this.tMaxRequested = undefined;
1631 this.tMinRequested = undefined;
1632 this.tMinReceived = undefined;
1633}
1634
1635function binarySearch(array, target) {
1636 let startIndex = 0;
1637 let endIndex = array.length - 1;
1638 let middleIndex;
1639 while (startIndex <= endIndex) {
1640 middleIndex = Math.floor((startIndex + endIndex) / 2);
1641 if (target === array[middleIndex])
1642 return middleIndex;
1643
1644 if (target > array[middleIndex])
1645 startIndex = middleIndex + 1;
1646 if (target < array[middleIndex])
1647 endIndex = middleIndex - 1;
1648 }
1649
1650 return middleIndex;
1651}
1652
1653function splitEventAndTagName(var_name) {
1654 let colons = [];
1655
1656 for (let i = 0; i < var_name.length; i++) {
1657 if (var_name[i] == ':') {
1658 colons.push(i);
1659 }
1660 }
1661
1662 let slash_pos = var_name.indexOf("/");
1663 let uses_per_variable_naming = (slash_pos != -1);
1664
1665 if (uses_per_variable_naming && colons.length % 2 == 1) {
1666 let middle_colon_pos = colons[Math.floor(colons.length / 2)];
1667 let slash_to_mid = var_name.substr(slash_pos + 1, middle_colon_pos - slash_pos - 1);
1668 let mid_to_end = var_name.substr(middle_colon_pos + 1);
1669
1670 if (slash_to_mid == mid_to_end) {
1671 // Special case - we have a string of the form Beamlime/GS2:FC1:GS2:FC1.
1672 // Logger has already warned people that having colons in the equipment/event
1673 // names is a bad idea, so we only need to worry about them in the tag name.
1674 split_pos = middle_colon_pos;
1675 } else {
1676 // We have a string of the form Beamlime/Demand:GS2:FC1. Split at the first colon.
1677 split_pos = colons[0];
1678 }
1679 } else {
1680 // Normal case - split at the fist colon.
1681 split_pos = colons[0];
1682 }
1683
1684 let event_name = var_name.substr(0, split_pos);
1685 let tag_name = var_name.substr(split_pos + 1);
1686
1687 return [event_name, tag_name];
1688}
1689
1690MhistoryGraph.prototype.mouseEvent = function (e) {
1691
1692 // fix buttons for IE
1693 if (!e.which && e.button) {
1694 if ((e.button & 1) > 0) e.which = 1; // Left
1695 else if ((e.button & 4) > 0) e.which = 2; // Middle
1696 else if ((e.button & 2) > 0) e.which = 3; // Right
1697 }
1698
1699 // extract X and Y coordinates
1700 let eventX, eventY;
1701
1702 if (e.type === "touchstart" || e.type === "touchmove") {
1703 eventX = e.touches[0].clientX;
1704 eventY = e.touches[0].clientY;
1705 let rect = e.target.getBoundingClientRect();
1706 eventX = eventX - Math.round(rect.left);
1707 eventY = eventY - Math.round(rect.top);
1708 } else if (e.type === "mousedown" || e.type === "mousemove" || e.type === "mouseup" ||
1709 e.type === "dblclick") {
1710 eventX = e.offsetX;
1711 eventY = e.offsetY;
1712 }
1713
1714 let cursor = this.pendingUpdates > 0 ? "progress" : "default";
1715 let title = "";
1716 let cancel = false;
1717
1718 // cancel dragging in case we did not catch the mouseup event
1719 if (e.type === "mousemove" && e.buttons === 0 &&
1720 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
1721 cancel = true;
1722
1723 if (e.type === "mousedown" || (e.type === "touchstart" && e.touches.length === 1)) {
1724
1725 this.intSelector.style.display = "none";
1726 this.downloadSelector.style.display = "none";
1727
1728 // check for buttons
1729 this.button.forEach(b => {
1730 if (eventX > b.x1 && eventX < b.x1 + b.width &&
1731 eventY > b.y1 && eventY < b.y1 + b.width &&
1732 b.enabled) {
1733 b.click(this);
1734 }
1735 });
1736
1737 // check for zoom buttons
1738 let xb;
1739 if (this.showMenuButtons)
1740 xb = this.width - 26 - 40;
1741 else
1742 xb = this.width - 41;
1743 if (eventX > xb && eventX < xb + 20 &&
1744 eventY > this.y1 - 20 && eventY < this.y1) {
1745 // zoom in
1746 let delta = this.tMax - this.tMin;
1747 if (this.scroll) {
1748 this.tMin += delta / 2; // only zoom on left side in scroll mode
1749 } else {
1750 this.tMin += delta / 4;
1751 this.tMax -= delta / 4; // zoom to center
1752 }
1753
1754 this.loadFullData(this.tMin, this.tMax);
1755
1756 if (this.callbacks.timeZoom !== undefined)
1757 this.callbacks.timeZoom(this);
1758
1759 e.preventDefault();
1760 return;
1761 }
1762 if (eventX > xb + 20 && eventX < xb + 40 &&
1763 eventY > this.y1 - 20 && eventY < this.y1) {
1764 // zoom out
1765 if (this.pendingUpdates > 0) {
1766 dlgMessage("Warning", "Don't press the '-' too fast!", true, false);
1767 } else {
1768 let delta = this.tMax - this.tMin;
1769 this.tMin -= delta / 2;
1770 this.tMax += delta / 2;
1771 // don't go into the future
1772 let now = Math.floor(new Date() / 1000);
1773 if (this.tMax > now) {
1774 this.tMax = now;
1775 this.tMin = now - 2*delta;
1776 }
1777
1778 this.loadFullData(this.tMin, this.tMax);
1779
1780 if (this.callbacks.timeZoom !== undefined)
1781 this.callbacks.timeZoom(this);
1782 }
1783
1784 if (e.type === "mousedown" ) {
1785 e.preventDefault();
1786 return;
1787 }
1788 }
1789
1790 // check for dragging
1791 if (eventX > this.x1 && eventX < this.x2 &&
1792 eventY > this.y2 && eventY < this.y1) {
1793 this.drag.active = true;
1794 this.marker.active = false;
1795 this.scroll = false;
1796 this.drag.xStart = eventX;
1797 this.drag.yStart = eventY;
1798 this.drag.tStart = this.xToTime(eventX);
1799 this.drag.tMinStart = this.tMin;
1800 this.drag.tMaxStart = this.tMax;
1801 this.drag.yMinStart = this.yMin;
1802 this.drag.yMaxStart = this.yMax;
1803 this.drag.vStart = this.yToValue(eventY);
1804 }
1805
1806 // check for axis dragging
1807 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1) {
1808 this.zoom.x.active = true;
1809 this.scroll = false;
1810 this.zoom.x.x1 = eventX;
1811 this.zoom.x.x2 = undefined;
1812 this.zoom.x.t1 = this.xToTime(eventX);
1813 }
1814 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1) {
1815 this.zoom.y.active = true;
1816 this.scroll = false;
1817 this.zoom.y.y1 = eventY;
1818 this.zoom.y.y2 = undefined;
1819 this.zoom.y.v1 = this.yToValue(eventY);
1820 }
1821
1822 }
1823
1824 if (cancel || e.type === "mouseup" || e.type === "touchend") {
1825
1826 if (this.drag.active) {
1827 this.drag.active = false;
1828 }
1829
1830 if (this.zoom.x.active) {
1831 if (this.zoom.x.x2 !== undefined &&
1832 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
1833 let t1 = this.zoom.x.t1;
1834 let t2 = this.xToTime(this.zoom.x.x2);
1835 if (t1 > t2)
1836 [t1, t2] = [t2, t1];
1837 if (t2 - t1 < 1)
1838 t1 -= 1;
1839 this.tMin = t1;
1840 this.tMax = t2;
1841 }
1842 this.zoom.x.active = false;
1843
1844 this.loadFullData(this.tMin, this.tMax);
1845
1846 if (this.callbacks.timeZoom !== undefined)
1847 this.callbacks.timeZoom(this);
1848 }
1849
1850 if (this.zoom.y.active) {
1851 if (this.zoom.y.y2 !== undefined &&
1852 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
1853 let v1 = this.zoom.y.v1;
1854 let v2 = this.yToValue(this.zoom.y.y2);
1855 if (v1 > v2)
1856 [v1, v2] = [v2, v1];
1857 this.yMin = v1;
1858 this.yMax = v2;
1859 }
1860 this.zoom.y.active = false;
1861 this.yZoom = true;
1862 this.findMinMax();
1863 this.redraw(true);
1864 }
1865
1866 }
1867
1868 if (e.type === "touchstart" && e.touches.length === 2) {
1869
1870 // start pinch / zoom
1871
1872 let rect = e.target.getBoundingClientRect();
1873
1874 this.zoom.x.pinch = true;
1875 this.zoom.x.x1 = e.touches[0].clientX - Math.round(rect.left);
1876 this.zoom.x.x2 = e.touches[1].clientX - Math.round(rect.left);
1877 this.zoom.x.t1 = this.xToTime(this.zoom.x.x1);
1878 this.zoom.x.t2 = this.xToTime(this.zoom.x.x2);
1879
1880 this.zoom.y.pinch = true;
1881 this.zoom.y.y1 = e.touches[0].clientY - Math.round(rect.top);
1882 this.zoom.y.y2 = e.touches[1].clientY - Math.round(rect.top);
1883 this.zoom.y.v1 = this.yToValue(this.zoom.y.y1);
1884 this.zoom.y.v2 = this.yToValue(this.zoom.y.y2);
1885
1886 let w = Math.abs(this.zoom.x.x2 - this.zoom.x.x1);
1887 let h = Math.abs(this.zoom.y.y2 - this.zoom.y.y1);
1888
1889 if (w < h/4)
1890 this.zoom.x.pinch = false;
1891 if (h < w/4)
1892 this.zoom.y.pinch = false;
1893
1894 if (this.zoom.y.pinch)
1895 this.yZoom = true;
1896
1897 }
1898
1899 if (e.type === "touchmove" && e.touches.length === 2) {
1900
1901 // pinch / zoom
1902
1903 let rect = e.target.getBoundingClientRect();
1904 let x1 = e.touches[0].clientX - Math.round(rect.left);
1905 let x2 = e.touches[1].clientX - Math.round(rect.left);
1906 let y1 = e.touches[0].clientY - Math.round(rect.top);
1907 let y2 = e.touches[1].clientY - Math.round(rect.top);
1908
1909 // solution to linear equation:
1910 // xToTime(x1) =!= this.zoom.x.t1
1911 // xToTime(x2) =!= this.zoom.x.t2
1912
1913 if (this.zoom.x.pinch) {
1914 let a = (x1 - this.x1) / (this.x2 - this.x1);
1915 let b = (x2 - this.x1) / (this.x2 - this.x1);
1916
1917 let tMin = (a * this.zoom.x.t2 - b * this.zoom.x.t1) / (a - b);
1918 let tMax = ((a - 1) * this.zoom.x.t2 - (b - 1) * this.zoom.x.t1) / (a - b);
1919
1920 if (tMax > tMin + 10) {
1921 this.tMin = tMin;
1922 this.tMax = tMax;
1923 }
1924
1925 this.loadSideData();
1926 }
1927
1928 if (this.zoom.y.pinch) {
1929 let a = (this.y1 - y1) / (this.y1 - this.y2);
1930 let b = (this.y1 - y2) / (this.y1 - this.y2);
1931
1932 let yMin = (a * this.zoom.y.v2 - b * this.zoom.y.v1) / (a - b);
1933 let yMax = ((a - 1) * this.zoom.y.v2 - (b - 1) * this.zoom.y.v1) / (a - b);
1934
1935 if (yMax > yMin) {
1936 this.yMin = yMin;
1937 this.yMax = yMax;
1938 }
1939 }
1940
1941 this.redraw();
1942 }
1943
1944 if (e.type === "touchend") {
1945 if (this.zoom.x.pinch)
1946 this.loadFullData(this.tMin, this.tMax);
1947
1948 this.zoom.x.pinch = false;
1949 this.zoom.y.pinch = false;
1950 }
1951
1952 if (e.type === "mousemove" || ((e.type === "touchmove" || e.type === "touchstart") && e.touches.length === 1) ) {
1953
1954 if (this.drag.active) {
1955
1956 // execute dragging
1957 cursor = "move";
1958 let dt = Math.round((eventX - this.drag.xStart) / (this.x2 - this.x1) * (this.tMax - this.tMin));
1959 this.tMin = this.drag.tMinStart - dt;
1960 this.tMax = this.drag.tMaxStart - dt;
1961 this.drag.lastDt = (eventX - this.drag.lastOffsetX) / (this.x2 - this.x1) * (this.tMax - this.tMin);
1962 this.drag.lastT = new Date().getTime();
1963 this.drag.lastOffsetX = eventX;
1964
1965 if (this.yZoom) {
1966
1967 if (this.logAxis) {
1968
1969 let dy = eventY - this.drag.yStart;
1970
1971 this.yMin = Math.exp((this.y1 - (this.y1 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
1972 this.yMax = Math.exp((this.y1 - (this.y2 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
1973
1974 if (this.yMin <= 0)
1975 this.yMin = 1E-20;
1976 if (this.yMax <= 0)
1977 this.yMax = 1E-18;
1978
1979 } else {
1980 let dy = (this.drag.yStart - eventY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
1981 this.yMin = this.drag.yMinStart - dy;
1982 this.yMax = this.drag.yMaxStart - dy;
1983 }
1984
1985 }
1986
1987 this.loadSideData();
1988 this.findMinMax();
1989 this.redraw();
1990
1991 if (this.callbacks.timeZoom !== undefined)
1992 this.callbacks.timeZoom(this);
1993
1994 }
1995
1996 if (!this.drag.active || e.type === "touchstart" || e.type === "touchmove") {
1997
1998 let redraw = false;
1999
2000 // change cursor to pointer over buttons
2001 this.button.forEach(b => {
2002 if (eventX > b.x1 && eventY > b.y1 &&
2003 eventX < b.x1 + b.width && eventY < b.y1 + b.height) {
2004 cursor = "pointer";
2005 title = b.title;
2006 }
2007 });
2008
2009 if (this.showZoomButtons) {
2010
2011 let xb;
2012 if (this.showMenuButtons)
2013 xb = this.width - 26 - 40;
2014 else
2015 xb = this.width - 41;
2016
2017 // check for zoom buttons
2018 if (eventX > xb && eventX < xb + 20 &&
2019 eventY > this.y1 - 20 && eventY < this.y1) {
2020 cursor = "pointer";
2021 title = "Zoom in";
2022 }
2023 if (eventX > xb + 20 && eventX < xb + 40 &&
2024 eventY > this.y1 - 20 && eventY < this.y1) {
2025 cursor = "pointer";
2026 title = "Zoom out";
2027 }
2028 }
2029
2030 // display zoom cursor
2031 if (eventX > this.x1 && eventX < this.x2 && eventY > this.y1)
2032 cursor = "ew-resize";
2033 if (eventY < this.y1 && eventY > this.y2 && eventX < this.x1)
2034 cursor = "ns-resize";
2035
2036 // execute axis zoom
2037 if (this.zoom.x.active) {
2038 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, eventX));
2039 this.zoom.x.t2 = this.xToTime(eventX);
2040 redraw = true;
2041 }
2042 if (this.zoom.y.active) {
2043 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, eventY));
2044 this.zoom.y.v2 = this.yToValue(eventY);
2045 redraw = true;
2046 }
2047
2048 // check if cursor close to graph point
2049 if (this.data !== undefined && this.x.length && this.y.length) {
2050
2051 let minDist = 10000;
2052 let markerX, markerY, markerT, markerV;
2053 for (let di = 0; di < this.data.length; di++) {
2054
2055 if (this.solo.active && di !== this.solo.index)
2056 continue;
2057
2058 let i1 = binarySearch(this.x[di], eventX - 10);
2059 let i2 = binarySearch(this.x[di], eventX + 10);
2060
2061 if (!this.binned) {
2062 for (let i = i1; i <= i2; i++) {
2063 let d = (eventX - this.x[di][i]) * (eventX - this.x[di][i]) +
2064 (eventY - this.y[di][i]) * (eventY - this.y[di][i]);
2065 if (d < minDist) {
2066 minDist = d;
2067 markerX = this.x[di][i];
2068 markerY = this.y[di][i];
2069 markerT = this.t[di][i];
2070 markerV = this.v[di][i];
2071
2072 if (this.param["Show raw value"] !== undefined &&
2073 this.param["Show raw value"][di])
2074 markerV = this.vRaw[di][i];
2075 else
2076 markerV = this.v[di][i];
2077
2078 this.marker.graphIndex = di;
2079 this.marker.index = i;
2080 }
2081 }
2082 } else {
2083
2084 // check max values
2085 for (let i = i1; i <= i2; i++) {
2086 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2087 (eventY - this.p[di][i].max) * (eventY - this.p[di][i].max);
2088 if (d < minDist) {
2089 minDist = d;
2090 markerX = this.p[di][i].x;
2091 markerY = this.p[di][i].max;
2092 markerT = this.p[di][i].t;
2093
2094 if (this.param["Show raw value"] !== undefined &&
2095 this.param["Show raw value"][di])
2096 markerV = this.p[di][i].rawMaxValue;
2097 else
2098 markerV = this.p[di][i].maxValue;
2099
2100 this.marker.graphIndex = di;
2101 this.marker.index = i;
2102 }
2103 }
2104
2105 // check min values
2106 for (let i = i1; i <= i2; i++) {
2107 let d = (eventX - this.p[di][i].x) * (eventX - this.p[di][i].x) +
2108 (eventY - this.p[di][i].min) * (eventY - this.p[di][i].min);
2109 if (d < minDist) {
2110 minDist = d;
2111 markerX = this.p[di][i].x;
2112 markerY = this.p[di][i].min;
2113 markerT = this.p[di][i].t;
2114
2115 if (this.param["Show raw value"] !== undefined &&
2116 this.param["Show raw value"][di])
2117 markerV = this.p[di][i].rawMinValue;
2118 else
2119 markerV = this.p[di][i].minValue;
2120
2121 this.marker.graphIndex = di;
2122 this.marker.index = i;
2123 }
2124 }
2125
2126 }
2127 }
2128
2129 // exclude zoom buttons if visible
2130 let exclude = false;
2131 if (this.showZoomButtons &&
2132 eventX > this.width - 26 - 40 && this.offsetX < this.width - 26 &&
2133 eventY > this.y1 - 20 && eventY < this.y1) {
2134 exclude = true;
2135 }
2136 // exclude label area
2137 if (this.showLabels &&
2138 eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7 &&
2139 eventY > this.y2 && eventY < this.y2 + this.variablesHeight + 2) {
2140 exclude = true;
2141 }
2142
2143 if (exclude) {
2144 this.marker.active = false;
2145 } else {
2146 this.marker.active = Math.sqrt(minDist) < 10 && eventX > this.x1 && eventX < this.x2;
2147 if (this.marker.active) {
2148 this.marker.x = markerX;
2149 this.marker.y = markerY;
2150 this.marker.t = markerT;
2151 this.marker.v = markerV;
2152
2153 this.marker.mx = eventX;
2154 this.marker.my = eventY;
2155 }
2156 }
2157 if (this.marker.active)
2158 redraw = true;
2159 if (!this.marker.active && this.marker.activeOld)
2160 redraw = true;
2161 this.marker.activeOld = this.marker.active;
2162
2163 if (redraw)
2164 this.redraw(true);
2165 }
2166 }
2167
2168 }
2169
2170 if (e.type === "dblclick") {
2171
2172 // check if inside zoom buttons
2173 if (eventX > this.width - 26 - 40 && eventX < this.width - 26 &&
2174 eventY > this.y1 - 20 && eventY < this.y1) {
2175 // just ignore it
2176
2177 } else {
2178
2179 // measure distance to graphs
2180 if (this.data !== undefined && this.x.length && this.y.length) {
2181
2182 // check if inside label area
2183 let flag = false;
2184 if (this.showLabels) {
2185 if (eventX > this.x1 && eventX < this.x1 + 25 + this.variablesWidth + 7) {
2186 let i = Math.floor((eventY - (this.y2 + 4)) / 17);
2187 if (i < this.data.length) {
2188 this.solo.active = true;
2189 this.solo.index = i;
2190 this.findMinMax();
2191 flag = true;
2192 }
2193 }
2194 }
2195
2196 if (!flag) {
2197 let minDist = 100;
2198 for (let di = 0; di < this.data.length; di++) {
2199 for (let i = 0; i < this.x[di].length; i++) {
2200 if (this.x[di][i] > this.x1 && this.x[di][i] < this.x2) {
2201 let d = Math.sqrt(Math.pow(eventX - this.x[di][i], 2) +
2202 Math.pow(eventY - this.y[di][i], 2));
2203 if (d < minDist) {
2204 minDist = d;
2205 this.solo.index = di;
2206 }
2207 }
2208 }
2209 }
2210 // check if close to graph point
2211 if (minDist < 10 && eventX > this.x1 && eventX < this.x2)
2212 this.solo.active = !this.solo.active;
2213 this.findMinMax();
2214 }
2215
2216 this.redraw(true);
2217 }
2218 }
2219 }
2220
2221 this.parentDiv.title = title;
2222 this.parentDiv.style.cursor = cursor;
2223
2224 e.preventDefault();
2225};
2226
2227MhistoryGraph.prototype.mouseWheelEvent = function (e) {
2228
2229 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2230 e.offsetY > this.y2 && e.offsetY < this.y1) {
2231
2232 if (e.altKey || e.shiftKey) {
2233
2234 // zoom Y axis
2235 this.yZoom = true;
2236 let f = (e.offsetY - this.y1) / (this.y2 - this.y1);
2237
2238 let step = e.deltaY / 100;
2239 if (step > 0.5)
2240 step = 0.5;
2241 if (step < -0.5)
2242 step = -0.5;
2243
2244 let dtMin = f * (this.yMax - this.yMin) * step;
2245 let dtMax = (1 - f) * (this.yMax - this.yMin) * step;
2246
2247 if (((this.yMax + dtMax) - (this.yMin - dtMin)) / (this.yMax0 - this.yMin0) < 1000 &&
2248 (this.yMax0 - this.yMin0) / ((this.yMax + dtMax) - (this.yMin - dtMin)) < 1000) {
2249 this.yMin -= dtMin;
2250 this.yMax += dtMax;
2251
2252 if (this.logAxis && this.yMin <= 0)
2253 this.yMin = 1E-20;
2254 if (this.logAxis && this.yMax <= 0)
2255 this.yMax = 1E-18;
2256 }
2257
2258 this.redraw();
2259
2260 } else if (e.ctrlKey || e.metaKey) {
2261
2262 this.showZoomButtons = false;
2263
2264 // zoom time axis
2265 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2266 let m = e.deltaY / 100;
2267 if (m > 0.3)
2268 m = 0.3;
2269 if (m < -0.3)
2270 m = -0.3;
2271 let dtMin = Math.abs(f * (this.tMax - this.tMin) * m);
2272 let dtMax = Math.abs((1 - f) * (this.tMax - this.tMin) * m);
2273
2274 if (e.deltaY < 0) {
2275 // zoom in
2276 if (this.scroll) {
2277 this.tMin += dtMin;
2278 } else {
2279 this.tMin += dtMin;
2280 this.tMax -= dtMax;
2281 }
2282
2283 this.loadFullData(this.tMin, this.tMax);
2284 }
2285 if (e.deltaY > 0) {
2286 // zoom out
2287 if (this.scroll) {
2288 this.tMin -= dtMin;
2289 } else {
2290 this.tMin -= dtMin;
2291 this.tMax += dtMax;
2292 }
2293
2294 this.loadFullData(this.tMin, this.tMax);
2295 }
2296
2297 if (this.callbacks.timeZoom !== undefined)
2298 this.callbacks.timeZoom(this);
2299
2300 } else if (e.deltaX !== 0) {
2301
2302 let dt = (this.tMax - this.tMin) / 1000 * e.deltaX;
2303 this.tMin += dt;
2304 this.tMax += dt;
2305
2306 if (dt < 0)
2307 this.loadSideData();
2308 this.findMinMax();
2309 this.redraw();
2310 } else
2311 return;
2312
2313 this.marker.active = false;
2314
2315 e.preventDefault();
2316 }
2317};
2318
2319MhistoryGraph.prototype.resetAxes = function () {
2320 this.tMax = Math.floor(new Date() / 1000);
2321 this.tMin = this.tMax - this.tScale;
2322
2323 this.scroll = true;
2324 this.yZoom = false;
2325 this.showZoomButtons = true;
2326 this.loadFullData(this.tMin, this.tMax);
2327};
2328
2329MhistoryGraph.prototype.jumpToCurrent = function () {
2330 let dt = Math.floor(this.tMax - this.tMin);
2331
2332 // limit to one week maximum (otherwise we have to read binned data)
2333 if (dt > 24*3600*7)
2334 dt = 24*3600*7;
2335
2336 this.tMax = Math.floor(new Date() / 1000);
2337 this.tMin = this.tMax - dt;
2338 this.scroll = true;
2339
2340 this.loadFullData(this.tMin, this.tMax);
2341};
2342
2343MhistoryGraph.prototype.setTimespan = function (tMin, tMax, scroll) {
2344 this.tMin = tMin;
2345 this.tMax = tMax;
2346 this.scroll = scroll;
2347
2348 this.loadFullData(tMin, tMax, scroll);
2349};
2350
2351MhistoryGraph.prototype.resize = function () {
2352 this.canvas.width = this.parentDiv.clientWidth;
2353 this.canvas.height = this.parentDiv.clientHeight;
2354 this.width = this.parentDiv.clientWidth;
2355 this.height = this.parentDiv.clientHeight;
2356
2357 if (this.intSelector !== undefined)
2358 this.intSelector.style.display = "none";
2359
2360 this.forceConvert = true;
2361 this.redraw(true);
2362};
2363
2364MhistoryGraph.prototype.redraw = function (force) {
2365 this.forceRedraw = force;
2366 let f = this.draw.bind(this);
2367 window.requestAnimationFrame(f);
2368};
2369
2370MhistoryGraph.prototype.timeToXInit = function () {
2371 this.timeToXScale = 1 / (this.tMax - this.tMin) * (this.x2 - this.x1);
2372}
2373
2374MhistoryGraph.prototype.timeToX = function (t) {
2375 return (t - this.tMin) * this.timeToXScale + this.x1;
2376};
2377
2378MhistoryGraph.prototype.truncateInfinity = function(v) {
2379 if (v === Infinity) {
2380 return Number.MAX_VALUE;
2381 } else if (v === -Infinity) {
2382 return -Number.MAX_VALUE;
2383 } else {
2384 return v;
2385 }
2386};
2387
2388MhistoryGraph.prototype.valueToYInit = function () {
2389 // Avoid overflow of max - min > inf
2390 let max_scaled = this.yMax / 1e4;
2391 let min_scaled = this.yMin / 1e4;
2392 this.valueToYScale = (this.y1 - this.y2) * 1e-4 / (max_scaled - min_scaled);
2393}
2394
2395MhistoryGraph.prototype.valueToY = function (v) {
2396 if (v === Infinity) {
2397 return this.yMax >= Number.MAX_VALUE ? this.y2 : 0;
2398 } else if (v === -Infinity) {
2399 return this.yMin <= -Number.MAX_VALUE ? this.y1 : this.y1 * 2;
2400 } else if (this.logAxis) {
2401 if (v <= 0)
2402 return this.y1;
2403 else
2404 return this.y1 - (Math.log(v) - Math.log(this.yMin)) /
2405 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
2406 } else {
2407 return this.y1 - (v - this.yMin) * this.valueToYScale;
2408 }
2409};
2410
2411MhistoryGraph.prototype.xToTime = function (x) {
2412 return (x - this.x1) / (this.x2 - this.x1) * (this.tMax - this.tMin) + this.tMin;
2413};
2414
2415MhistoryGraph.prototype.yToValue = function (y) {
2416 if (!isFinite(this.yMax - this.yMin)) {
2417 // Contortions to avoid Infinity.
2418 let scaled = (this.yMax / 1e4) - (this.yMin / 1e4);
2419 let retval = ((((this.y1 - y) / (this.y1 - this.y2)) * scaled) + (this.yMin / 1e4)) * 1e4;
2420 return retval;
2421 }
2422 if (this.logAxis) {
2423 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
2424 return Math.exp(yl);
2425 }
2426 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
2427};
2428
2429MhistoryGraph.prototype.findMinMax = function () {
2430
2431 if (this.yZoom)
2432 return;
2433
2434 if (!this.autoscaleMin)
2435 this.yMin0 = this.param["Minimum"];
2436
2437 if (!this.autoscaleMax)
2438 this.yMax0 = this.param["Maximum"];
2439
2440 if (!this.autoscaleMin && !this.autoscaleMax) {
2441 this.yMin = this.yMin0;
2442 this.yMax = this.yMax0;
2443 return;
2444 }
2445
2446 let minValue = undefined;
2447 let maxValue = undefined;
2448 for (let index = 0; index < this.data.length; index++) {
2449 if (this.events[index] === "Run transitions")
2450 continue;
2451 if (this.data[index].time.length === 0)
2452 continue;
2453 if (this.solo.active && this.solo.index !== index)
2454 continue;
2455 let i1 = binarySearch(this.data[index].time, this.tMin) + 1;
2456 let i2 = binarySearch(this.data[index].time, this.tMax);
2457 while ((minValue === undefined ||
2458 maxValue === undefined ||
2459 Number.isNaN(minValue) ||
2460 Number.isNaN(maxValue)) &&
2461 i1 < i2) {
2462 // find first valid value
2463 if (this.binned) {
2464 if (this.data[index].bin[i1].count !== 0) {
2465 minValue = this.data[index].bin[i1].minValue;
2466 maxValue = this.data[index].bin[i1].maxValue;
2467 }
2468 } else {
2469 minValue = this.data[index].value[i1];
2470 maxValue = this.data[index].value[i1];
2471 }
2472 i1++;
2473 }
2474 for (let i = i1; i <= i2; i++) {
2475 if (this.binned) {
2476 if (this.data[index].bin[i].count === 0)
2477 continue;
2478 let v = this.data[index].bin[i].minValue;
2479 if (v < minValue)
2480 minValue = v;
2481 v = this.data[index].bin[i].maxValue;
2482 if (v > maxValue)
2483 maxValue = v;
2484 } else {
2485 let v = this.data[index].value[i];
2486 if (Number.isNaN(v))
2487 continue;
2488 if (v < minValue)
2489 minValue = v;
2490 if (v > maxValue)
2491 maxValue = v;
2492 }
2493 }
2494 }
2495
2496 // array could be empty (no data), so min/max would be NaN
2497 if (Number.isNaN(minValue) || Number.isNaN(maxValue))
2498 minValue = maxValue = 0;
2499
2500 if (this.autoscaleMin)
2501 this.yMin0 = this.yMin = minValue;
2502 if (this.autoscaleMax)
2503 this.yMax0 = this.yMax = maxValue;
2504
2505 if (minValue === undefined || maxValue === undefined) {
2506 this.yMin0 = -0.5;
2507 this.yMax0 = 0.5;
2508 }
2509
2510 if (this.yMin0 === this.yMax0) {
2511 this.yMin0 -= 0.5;
2512 this.yMax0 += 0.5;
2513 }
2514
2515 if (this.yMax0 < this.yMin0)
2516 this.yMax0 = this.yMin0 + 1;
2517
2518 if (!this.yZoom) {
2519 if (this.autoscaleMin) {
2520 if (this.logAxis)
2521 this.yMin = 0.8 * this.yMin0;
2522 else
2523 // leave 10% space below graph
2524 this.yMin = this.yMin0 - this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2525 } else
2526 this.yMin = this.yMin0;
2527 if (this.logAxis && this.yMin <= 0)
2528 this.yMin = 1E-20;
2529
2530 if (this.autoscaleMax) {
2531 if (this.logAxis)
2532 this.yMax = 1.2 * this.yMax0;
2533 else
2534 // leave 10% space above graph
2535 this.yMax = this.yMax0 + this.truncateInfinity(this.yMax0 - this.yMin0) / 10;
2536 } else
2537 this.yMax = this.yMax0;
2538 if (this.logAxis && this.yMax <= 0)
2539 this.yMax = 1E-18;
2540 }
2541
2542 this.yMax = this.truncateInfinity(this.yMax)
2543 this.yMin = this.truncateInfinity(this.yMin)
2544};
2545
2546function convertLastWritten(last) {
2547 if (last === 0)
2548 return "no data available";
2549
2550 let d = new Date(last * 1000).toLocaleDateString(
2551 'en-GB', {
2552 day: '2-digit', month: 'short', year: '2-digit',
2553 hour12: false, hour: '2-digit', minute: '2-digit'
2554 }
2555 );
2556
2557 return "last data: " + d;
2558}
2559
2560MhistoryGraph.prototype.updateURL = function() {
2561 let url = window.location.href;
2562 if (url.search("&A=") !== -1)
2563 url = url.slice(0, url.search("&A="));
2564 url += "&A=" + Math.round(this.tMin) + "&B=" + Math.round(this.tMax);
2565
2566 if (url !== window.location.href)
2567 window.history.replaceState({}, "Midas History", url);
2568}
2569
2570function createPinstripeCanvas() {
2571 const patternCanvas = document.createElement("canvas");
2572 const pctx = patternCanvas.getContext('2d', { antialias: true });
2573 const colour = "#FFC0C0";
2574
2575 const CANVAS_SIDE_LENGTH = 90;
2576 const WIDTH = CANVAS_SIDE_LENGTH;
2577 const HEIGHT = CANVAS_SIDE_LENGTH;
2578 const DIVISIONS = 4;
2579
2580 patternCanvas.width = WIDTH;
2581 patternCanvas.height = HEIGHT;
2582 pctx.fillStyle = colour;
2583
2584 // Top line
2585 pctx.beginPath();
2586 pctx.moveTo(0, HEIGHT * (1 / DIVISIONS));
2587 pctx.lineTo(WIDTH * (1 / DIVISIONS), 0);
2588 pctx.lineTo(0, 0);
2589 pctx.lineTo(0, HEIGHT * (1 / DIVISIONS));
2590 pctx.fill();
2591
2592 // Middle line
2593 pctx.beginPath();
2594 pctx.moveTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2595 pctx.lineTo(WIDTH * (1 / DIVISIONS), HEIGHT);
2596 pctx.lineTo(0, HEIGHT);
2597 pctx.lineTo(0, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2598 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), 0);
2599 pctx.lineTo(WIDTH, 0);
2600 pctx.lineTo(WIDTH, HEIGHT * (1 / DIVISIONS));
2601 pctx.fill();
2602
2603 // Bottom line
2604 pctx.beginPath();
2605 pctx.moveTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2606 pctx.lineTo(WIDTH * ((DIVISIONS - 1) / DIVISIONS), HEIGHT);
2607 pctx.lineTo(WIDTH, HEIGHT);
2608 pctx.lineTo(WIDTH, HEIGHT * ((DIVISIONS - 1) / DIVISIONS));
2609 pctx.fill();
2610
2611 return patternCanvas;
2612}
2613
2614MhistoryGraph.prototype.draw = function () {
2615 //profile(true);
2616
2617 // draw maximal 30 times per second
2618 if (!this.forceRedraw) {
2619 if (new Date().getTime() < this.lastDrawTime + 30)
2620 return;
2621 this.lastDrawTime = new Date().getTime();
2622 }
2623 this.forceRedraw = false;
2624
2625 let update_last_written = false;
2626
2627 let ctx = this.canvas.getContext("2d");
2628
2629 ctx.fillStyle = this.color.background;
2630 ctx.fillRect(0, 0, this.width, this.height);
2631
2632 if (this.data === undefined) {
2633 ctx.lineWidth = 1;
2634 ctx.font = "14px sans-serif";
2635 ctx.strokeStyle = "#808080";
2636 ctx.fillStyle = "#808080";
2637 ctx.textAlign = "center";
2638 ctx.textBaseline = "middle";
2639 ctx.fillText("Data being loaded ...", this.width / 2, this.height / 2);
2640 return;
2641 }
2642
2643 ctx.lineWidth = 1;
2644 ctx.font = "14px sans-serif";
2645
2646 if (this.height === undefined || this.width === undefined)
2647 return;
2648 if (this.yMin === undefined || Number.isNaN(this.yMin))
2649 return;
2650 if (this.yMax === undefined || Number.isNaN(this.yMax))
2651 return;
2652
2653 let axisLabelWidth = this.drawVAxis(ctx, 50, this.height - 25, this.height - 35,
2654 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.logAxis, false);
2655
2656 if (axisLabelWidth === undefined)
2657 return;
2658
2659 if (this.showAxis) {
2660 this.x1 = axisLabelWidth + 15;
2661 this.y1 = this.height - 25;
2662 this.x2 = this.width - 26;
2663 this.y2 = 10;
2664 } else {
2665 this.x1 = 1;
2666 this.y1 = this.height - 1;
2667 this.x2 = this.width - 26;
2668 this.y2 = 1;
2669 }
2670
2671 if (this.showMenuButtons === false)
2672 this.x2 = this.width - 1;
2673
2674 // title
2675 if (!this.floating && // suppress title since this is already in the dialog box
2676 this.showTitle) {
2677 this.y2 = 26;
2678 ctx.strokeStyle = this.color.axis;
2679 ctx.fillStyle = "#F0F0F0";
2680 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, 20);
2681 ctx.fillRect(this.x1, 6, this.x2 - this.x1, 20);
2682 ctx.textAlign = "center";
2683 ctx.textBaseline = "middle";
2684 ctx.fillStyle = "#808080";
2685 let str = "";
2686 if (this.group !== undefined)
2687 str += this.group + " - " + this.panel;
2688 else if (this.historyVar !== undefined)
2689 str += this.historyVar;
2690
2691 if (this.debugString !== "")
2692 str += " - " + this.debugString;
2693
2694 ctx.fillText(str, (this.x2 + this.x1) / 2, 16);
2695
2696 // display binning
2697 let s = Math.round(this.binSize);
2698 ctx.textAlign = "right";
2699 ctx.fillText(s, this.x2 - 10, 16);
2700 }
2701
2702 // re-calculate axis scaling since x2, y2 might have been changed above
2703 this.timeToXInit(); // initialize scale factor t -> x
2704 this.valueToYInit(); // initialize scale factor v -> y
2705
2706 // draw axis
2707 ctx.strokeStyle = this.color.axis;
2708 ctx.drawLine(this.x1, this.y2, this.x2, this.y2);
2709 ctx.drawLine(this.x2, this.y2, this.x2, this.y1);
2710
2711 if (this.logAxis && this.yMin < 1E-20)
2712 this.yMin = 1E-20;
2713 if (this.logAxis && this.yMax < 1E-18)
2714 this.yMax = 1E-18;
2715 this.drawVAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
2716 -4, -7, -10, -12, this.x2 - this.x1, this.yMin, this.yMax, this.logAxis, true);
2717 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
2718 4, 7, 10, 10, this.y2 - this.y1, this.tMin, this.tMax);
2719
2720 // draw hatched area for "future"
2721 let t = new Date() / 1000;
2722 if (this.tMax > t) {
2723 let x = this.timeToX(t);
2724 if (x < this.x1)
2725 x = this.x1;
2726 ctx.fillStyle = ctx.createPattern(createPinstripeCanvas(), 'repeat');
2727 ctx.fillRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2728
2729 ctx.strokeStyle = this.color.axis;
2730 ctx.strokeRect(x, this.y2, this.x2 - x, this.y1 - this.y2);
2731 }
2732
2733 // determine precision
2734 let n_sig1, n_sig2;
2735 if (this.yMin === 0)
2736 n_sig1 = 1;
2737 else
2738 n_sig1 = Math.floor(Math.log(Math.abs(this.yMin)) / Math.log(10)) -
2739 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2740
2741 if (this.yMax === 0)
2742 n_sig2 = 1;
2743 else
2744 n_sig2 = Math.floor(Math.log(Math.abs(this.yMax)) / Math.log(10)) -
2745 Math.floor(Math.log(Math.abs((this.yMax - this.yMin) / 50)) / Math.log(10)) + 1;
2746
2747 n_sig1 = Math.max(n_sig1, n_sig2);
2748 n_sig1 = Math.max(1, n_sig1);
2749
2750 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2751 if (Math.abs(this.yMin) < 100000)
2752 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMin)) /
2753 Math.log(10) + 0.001) + 1);
2754 if (Math.abs(this.yMax) < 100000)
2755 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(this.yMax)) /
2756 Math.log(10) + 0.001) + 1);
2757
2758 if (isNaN(n_sig1))
2759 n_sig1 = 6;
2760
2761 this.yPrecision = Math.max(6, n_sig1); // use at least 5 digits
2762
2763 ctx.save();
2764 ctx.beginPath();
2765 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2766 ctx.clip();
2767
2768 //profile("drawinit");
2769
2770 let nPoints = 0;
2771 for (let di = 0; di < this.data.length; di++)
2772 nPoints += this.data[di].time.length;
2773
2774 // convert values to points if window has changed or number of points have changed
2775 if (this.tMin !== this.tMinOld || this.tMax !== this.tMaxOld ||
2776 this.yMin !== this.yMinOld || this.yMax !== this.yMaxOld ||
2777 nPoints !== this.nPointsOld || this.forceConvert) {
2778
2779 this.tMinOld = this.tMin;
2780 this.tMaxOld = this.tMax;
2781 this.yMinOld = this.yMin;
2782 this.yMaxOld = this.yMax;
2783 this.nPointsOld = nPoints;
2784 this.forceConvert = false;
2785
2786 //profile();
2787 for (let di = 0; di < this.data.length; di++) {
2788
2789 if (this.x[di] === undefined) {
2790 this.x[di] = []; // x/y contain visible part of graph
2791 this.y[di] = [];
2792 this.t[di] = []; // t/v contain time/value pairs corresponding to x/y
2793 this.v[di] = [];
2794 this.p[di] = [];
2795 this.vRaw[di] = []; // vRaw contains the value before the formula
2796 }
2797
2798 let n = 0;
2799
2800 if (this.data[di].time.length === 0)
2801 continue;
2802
2803 let i1 = binarySearch(this.data[di].time, this.tMin);
2804 if (i1 > 0)
2805 i1--; // add point to the left
2806 let i2 = binarySearch(this.data[di].time, this.tMax);
2807 if (i2 < this.data[di].time.length - 1)
2808 i2++; // add points to the right
2809
2810 // un-binned data
2811 if (!this.binned || this.events[di] === "Run transitions") {
2812 for (let i = i1; i <= i2; i++) {
2813 let x = this.timeToX(this.data[di].time[i]);
2814 let y = this.valueToY(this.data[di].value[i]);
2815 if (y < -100000)
2816 y = -100000;
2817 if (y > 100000)
2818 y = 100000;
2819 if (!Number.isNaN(y)) {
2820 this.x[di][n] = x;
2821 this.y[di][n] = y;
2822 this.t[di][n] = this.data[di].time[i];
2823 this.v[di][n] = this.data[di].value[i];
2824 if (this.data[di].rawValue)
2825 this.vRaw[di][n] = this.data[di].rawValue[i];
2826 n++;
2827 }
2828 }
2829
2830 // truncate arrays if now shorter
2831 this.x[di].length = n;
2832 this.y[di].length = n;
2833 this.t[di].length = n;
2834 this.v[di].length = n;
2835 if (this.data[di].rawValue)
2836 this.vRaw[di].length = n;
2837
2838 } else {
2839
2840 // binned data
2841 for (let i = i1; i <= i2; i++) {
2842
2843 if (this.data[di].bin[i].count === 0)
2844 continue;
2845
2846 let p = {};
2847 p.n = this.data[di].bin[i].count;
2848 p.x = Math.round(this.timeToX(this.data[di].time[i]));
2849 p.t = this.data[di].time[i];
2850
2851 p.first = this.valueToY(this.data[di].bin[i].firstValue);
2852 p.min = this.valueToY(this.data[di].bin[i].minValue);
2853 p.minValue = this.data[di].bin[i].minValue;
2854 p.max = this.valueToY(this.data[di].bin[i].maxValue);
2855 p.maxValue = this.data[di].bin[i].maxValue;
2856 p.last = this.valueToY(this.data[di].bin[i].lastValue);
2857
2858 if (this.data[di].binRaw) {
2859 p.rawFirstValue = this.data[di].binRaw[i].firstValue;
2860 p.rawMinValue = this.data[di].binRaw[i].minValue;
2861 p.rawMaxValue = this.data[di].binRaw[i].maxValue;
2862 p.rawLastValue = this.data[di].binRaw[i].lastValue;
2863 }
2864
2865 this.p[di][n] = p;
2866
2867 this.x[di][n] = p.x;
2868
2869 n++;
2870 }
2871
2872 // truncate arrays if now shorter
2873 this.p[di].length = n;
2874 this.x[di].length = n;
2875 if (this.data[di].rawValue)
2876 this.vRaw[di].length = n;
2877 }
2878 }
2879 }
2880
2881 // draw shaded areas
2882 if (this.showFill) {
2883 for (let di = 0; di < this.data.length; di++) {
2884 if (this.solo.active && this.solo.index !== di)
2885 continue;
2886
2887 if (this.events[di] === "Run transitions")
2888 continue;
2889
2890 ctx.fillStyle = this.param["Colour"][di];
2891
2892 // don't draw lines over "gaps"
2893 let gap = this.timeToXScale * 600; // 10 min
2894 if (gap < 5)
2895 gap = 5; // minimum of 5 pixels
2896
2897 if (this.binned) {
2898 if (this.p[di].length > 0) {
2899 let p = this.p[di][0];
2900 let x0 = p.x;
2901 let xold = p.x;
2902 let y0 = p.first;
2903 ctx.beginPath();
2904 ctx.moveTo(p.x, p.first);
2905 ctx.lineTo(p.x, p.last);
2906 for (let i = 1; i < this.p[di].length; i++) {
2907 p = this.p[di][i];
2908 if (p.x - xold < gap) {
2909 ctx.lineTo(p.x, p.first);
2910 ctx.lineTo(p.x, p.last);
2911 } else {
2912 ctx.lineTo(xold, this.valueToY(0));
2913 ctx.lineTo(p.x, this.valueToY(0));
2914 ctx.lineTo(p.x, p.first);
2915 ctx.lineTo(p.x, p.last);
2916 }
2917 xold = p.x;
2918 }
2919 ctx.lineTo(xold, this.valueToY(0));
2920 ctx.lineTo(x0, this.valueToY(0));
2921 ctx.lineTo(x0, y0);
2922 ctx.globalAlpha = 0.1;
2923 ctx.fill();
2924 ctx.globalAlpha = 1;
2925 }
2926 } else { // binned
2927 if (this.x[di].length > 0 && this.y[di].length > 0) {
2928 let x = this.x[di][0];
2929 let y = this.y[di][0];
2930 let x0 = x;
2931 let y0 = y;
2932 let xold = x;
2933 ctx.beginPath();
2934 ctx.moveTo(x, y);
2935 for (let i = 1; i < this.x[di].length; i++) {
2936 x = this.x[di][i];
2937 y = this.y[di][i];
2938 if (x - xold < gap)
2939 ctx.lineTo(x, y);
2940 else {
2941 ctx.lineTo(xold, this.valueToY(0));
2942 ctx.lineTo(x, this.valueToY(0));
2943 ctx.lineTo(x, y);
2944 }
2945 xold = x;
2946 }
2947 ctx.lineTo(xold, this.valueToY(0));
2948 ctx.lineTo(x0, this.valueToY(0));
2949 ctx.lineTo(x0, y0);
2950 ctx.globalAlpha = 0.1;
2951 ctx.fill();
2952 ctx.globalAlpha = 1;
2953 }
2954 }
2955 }
2956 }
2957
2958 // profile("Draw shaded areas");
2959
2960 // draw graphs
2961 for (let di = 0; di < this.data.length; di++) {
2962 if (this.solo.active && this.solo.index !== di)
2963 continue;
2964
2965 if (this.events[di] === "Run transitions") {
2966
2967 if (this.tags[di] === "State") {
2968 if (this.x[di].length < 200) {
2969 for (let i = 0; i < this.x[di].length; i++) {
2970 if (this.v[di][i] === 1) {
2971 ctx.strokeStyle = "#FF0000";
2972 ctx.fillStyle = "#808080";
2973 ctx.textAlign = "right";
2974 ctx.textBaseline = "top";
2975 ctx.fillText(this.v[di + 1][i], this.x[di][i] - 5, this.y2 + 3);
2976 } else if (this.v[di][i] === 3) {
2977 ctx.strokeStyle = "#00A000";
2978 ctx.fillStyle = "#808080";
2979 ctx.textAlign = "left";
2980 ctx.textBaseline = "top";
2981 ctx.fillText(this.v[di + 1][i], this.x[di][i] + 3, this.y2 + 3);
2982 } else {
2983 ctx.strokeStyle = "#F9A600";
2984 }
2985
2986 ctx.setLineDash([8, 2]);
2987 ctx.drawLine(Math.floor(this.x[di][i]), this.y1, Math.floor(this.x[di][i]), this.y2);
2988 ctx.setLineDash([]);
2989 }
2990 }
2991 }
2992
2993 } else {
2994
2995 ctx.strokeStyle = this.param["Colour"][di];
2996
2997 // don't draw lines over "gaps"
2998 let gap = this.timeToXScale * 600; // 10 min
2999 if (gap < 5)
3000 gap = 5; // minimum of 5 pixels
3001
3002 if (this.binned) {
3003 if (this.p[di].length > 0) {
3004 let p = this.p[di][0];
3005 //console.log("di:" + di + " i:" + 0 + " x:" + p.x, " y:" + p.first);
3006 let xold = p.x;
3007 ctx.beginPath();
3008 ctx.moveTo(p.x, p.first);
3009 ctx.lineTo(p.x, p.max + 1); // in case min==max
3010 ctx.lineTo(p.x, p.min);
3011 ctx.lineTo(p.x, p.last);
3012 for (let i = 1; i < this.p[di].length; i++) {
3013 p = this.p[di][i];
3014 //console.log("di:" + di + " i:" + i + " x:" + p.x, " y:" + p.first);
3015 if (p.x - xold < gap) {
3016 // draw lines first - max - min - last
3017 ctx.lineTo(p.x, p.first);
3018 ctx.lineTo(p.x, p.max + 1); // in case min==max
3019 ctx.lineTo(p.x, p.min);
3020 ctx.lineTo(p.x, p.last);
3021 } else { // don't draw gap
3022 // draw lines first - max - min - last
3023 ctx.moveTo(p.x, p.first);
3024 ctx.lineTo(p.x, p.max + 1); // in case min==max
3025 ctx.lineTo(p.x, p.min);
3026 ctx.lineTo(p.x, p.last);
3027 }
3028 xold = p.x;
3029 }
3030 ctx.stroke();
3031 }
3032 } else { // binned
3033 if (this.x[di].length === 1) {
3034 let x = this.x[di][0];
3035 let y = this.y[di][0];
3036 ctx.fillStyle = this.param["Colour"][di];
3037 ctx.fillRect(x - 1, y - 1, 3, 3);
3038 } else {
3039 if (this.x[di].length > 0) {
3040 ctx.beginPath();
3041 let x = this.x[di][0];
3042 let y = this.y[di][0];
3043 let xold = x;
3044 ctx.moveTo(x, y);
3045 for (let i = 1; i < this.x[di].length; i++) {
3046 let x = this.x[di][i];
3047 let y = this.y[di][i];
3048 if (x - xold > gap)
3049 ctx.moveTo(x, y);
3050 else
3051 ctx.lineTo(x, y);
3052 xold = x;
3053 }
3054 ctx.stroke();
3055 }
3056 }
3057 }
3058 }
3059 }
3060
3061 ctx.restore(); // remove clipping
3062
3063 // profile("Draw graphs");
3064
3065 // labels with variable names and values
3066 if (this.showLabels) {
3067 if (this.solo.active)
3068 this.variablesHeight = 17 + 7;
3069 else
3070 this.variablesHeight = this.param["Variables"].length * 17 + 7;
3071 this.variablesWidth = 0;
3072
3073 // determine width of widest label
3074 this.param["Variables"].forEach((v, i) => {
3075 let width;
3076 if (this.param.Label[i] !== "") {
3077 width = ctx.measureText(this.param.Label[i]).width;
3078 } else {
3079 width = ctx.measureText(splitEventAndTagName(v)[1]).width;
3080 }
3081
3082 if (this.param["Show raw value"] !== undefined &&
3083 this.param["Show raw value"][i])
3084 width += ctx.measureText(" (Raw)").width;
3085
3086 width += 20; // space between name and value
3087
3088 if (this.v[i] !== undefined && this.v[i].length > 0) {
3089 // use last point in array
3090 let index = this.v[i].length - 1;
3091
3092 // use point at current marker
3093 if (this.marker.active)
3094 index = this.marker.index;
3095
3096 if (index < this.v[i].length) {
3097 let value;
3098 if (this.param["Show raw value"] !== undefined &&
3099 this.param["Show raw value"][i])
3100 value = this.vRaw[i][index];
3101 else
3102 value = this.v[i][index];
3103
3104 // convert value to string with 6 digits
3105 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3106 width += ctx.measureText(str).width;
3107 }
3108 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3109 // use last point in array
3110 let index = this.p[i].length - 1;
3111
3112 // use point at current marker
3113 if (this.marker.active)
3114 index = this.marker.index;
3115
3116 if (index < this.p[i].length) {
3117 let value;
3118 if (this.param["Show raw value"] !== undefined &&
3119 this.param["Show raw value"][i])
3120 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3121 else
3122 value = (this.p[i][index].minValue + this.p[i][index].maxValue)/2;
3123
3124 // convert value to string with 6 digits
3125 let str = " " + value.toPrecision(this.yPrecision).stripZeros();
3126 width += ctx.measureText(str).width;
3127 }
3128 } else {
3129 width += ctx.measureText(convertLastWritten(this.lastWritten[i])).width;
3130 }
3131
3132 this.variablesWidth = Math.max(this.variablesWidth, width);
3133 });
3134
3135 let xLabel = this.x1;
3136 if (this.solo.active)
3137 xLabel = this.x1 + 28;
3138
3139 ctx.save();
3140 ctx.beginPath();
3141 ctx.rect(xLabel, this.y2, 25 + this.variablesWidth + 7, this.variablesHeight + 2);
3142 ctx.clip();
3143
3144 ctx.strokeStyle = this.color.axis;
3145 ctx.fillStyle = "#F0F0F0";
3146 ctx.globalAlpha = 0.5;
3147 ctx.strokeRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3148 ctx.fillRect(xLabel, this.y2, 25 + this.variablesWidth + 5, this.variablesHeight);
3149 ctx.globalAlpha = 1;
3150
3151 this.param["Variables"].forEach((v, i) => {
3152
3153 if (this.solo.active && i !== this.solo.index)
3154 return;
3155
3156 let yLabel = 0;
3157 if (!this.solo.active)
3158 yLabel = i * 17;
3159
3160 ctx.lineWidth = 4;
3161 ctx.strokeStyle = this.param["Colour"][i];
3162 ctx.drawLine(xLabel + 5, this.y2 + 14 + yLabel, xLabel + 20, this.y2 + 14 + yLabel);
3163 ctx.lineWidth = 1;
3164
3165 ctx.textAlign = "left";
3166 ctx.textBaseline = "middle";
3167 ctx.fillStyle = "#404040";
3168
3169 let str;
3170 if (this.param.Label[i] !== "")
3171 str = this.param.Label[i];
3172 else
3173 str = splitEventAndTagName(v)[1];
3174
3175 if (this.param["Show raw value"] !== undefined &&
3176 this.param["Show raw value"][i])
3177 str += " (Raw)";
3178
3179 ctx.fillText(str, xLabel + 25, this.y2 + 14 + yLabel);
3180
3181 ctx.textAlign = "right";
3182
3183 // un-binned data
3184 if (this.v[i] !== undefined && this.v[i].length > 0) {
3185 // use last point in array
3186 let index = this.v[i].length - 1;
3187
3188 // use point at current marker
3189 if (this.marker.active)
3190 index = this.marker.index;
3191
3192 if (index < this.v[i].length) {
3193 // convert value to string with 6 digits
3194 let value;
3195 if (this.param["Show raw value"] !== undefined &&
3196 this.param["Show raw value"][i])
3197 value = this.vRaw[i][index];
3198 else
3199 value = this.v[i][index];
3200 let str = value.toPrecision(this.yPrecision).stripZeros();
3201 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3202 }
3203 } else if (this.p[i] !== undefined && this.p[i].length > 0) {
3204
3205 // binned data
3206
3207 // use last point in array
3208 let index = this.p[i].length - 1;
3209
3210 // use point at current marker
3211 if (this.marker.active)
3212 index = this.marker.index;
3213
3214 if (index < this.p[i].length) {
3215 // convert value to string with 6 digits
3216 let value;
3217 if (this.param["Show raw value"] !== undefined &&
3218 this.param["Show raw value"][i])
3219 value = (this.p[i][index].rawMinValue + this.p[i][index].rawMaxValue)/2;
3220 else
3221 value = (this.p[i][index].minValue + this.p[i][index].maxValue) / 2;
3222 let str = value.toPrecision(this.yPrecision).stripZeros();
3223 ctx.fillText(str, xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3224 }
3225
3226 } else {
3227
3228 if (this.lastWritten.length > 0) {
3229 if (this.lastWritten[i] > this.tMax) {
3230 //console.log("last written is in the future: " + this.events[i] + ", lw: " + this.lastWritten[i], ", this.tMax: " + this.tMax, ", diff: " + (this.lastWritten[i] - this.tMax));
3231 update_last_written = true;
3232 }
3233 ctx.fillText(convertLastWritten(this.lastWritten[i]),
3234 xLabel + 25 + this.variablesWidth, this.y2 + 14 + yLabel);
3235 } else {
3236 //console.log("last_written was not loaded yet");
3237 update_last_written = true;
3238 }
3239 }
3240
3241 });
3242
3243 ctx.restore(); // remove clipping
3244 }
3245
3246 // "updating" notice
3247 if (this.pendingUpdates > 0 && this.tMinReceived > this.tMin) {
3248 let str = "Updating data ...";
3249 ctx.strokeStyle = "#404040";
3250 ctx.fillStyle = "#FFC0C0";
3251 ctx.fillRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3252 ctx.strokeRect(this.x1 + 5, this.y1 - 22, 10 + ctx.measureText(str).width, 17);
3253 ctx.fillStyle = "#404040";
3254 ctx.textAlign = "left";
3255 ctx.textBaseline = "middle";
3256 ctx.fillText(str, this.x1 + 10, this.y1 - 13);
3257 }
3258
3259 let no_data = true;
3260
3261 for (let i = 0; i < this.data.length; i++) {
3262 if (this.data[i].time === undefined || this.data[i].time.length === 0) {
3263 } else {
3264 no_data = false;
3265 }
3266 }
3267
3268 // "empty window" notice
3269 if (no_data) {
3270 ctx.font = "16px sans-serif";
3271 let str = "No data available";
3272 ctx.strokeStyle = "#404040";
3273 ctx.fillStyle = "#F0F0F0";
3274 let w = ctx.measureText(str).width + 10;
3275 let h = 16 + 10;
3276 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3277 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
3278 ctx.fillStyle = "#404040";
3279 ctx.textAlign = "center";
3280 ctx.textBaseline = "middle";
3281 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
3282 ctx.font = "14px sans-serif";
3283 }
3284
3285 // buttons
3286 if (this.showMenuButtons) {
3287 let y = 0;
3288 let buttonSize = 20;
3289 this.button.forEach(b => {
3290 b.x1 = this.width - buttonSize - 6;
3291 b.y1 = 6 + y * (buttonSize + 4);
3292 b.width = buttonSize + 4;
3293 b.height = buttonSize + 4;
3294 b.enabled = true;
3295
3296 if (b.src === "maximize-2.svg") {
3297 let s = window.location.href;
3298 if (s.indexOf("&A") > -1)
3299 s = s.substr(0, s.indexOf("&A"));
3300 if (s === encodeURI(this.baseURL + "&group=" + this.group + "&panel=" + this.panel)) {
3301 b.enabled = false;
3302 return;
3303 }
3304 }
3305
3306 if (b.src === "corner-down-left.svg") {
3307 b.x1 = this.x1;
3308 b.y1 = this.y2;
3309 if (this.solo.active)
3310 b.enabled = true;
3311 else {
3312 b.enabled = false;
3313 return;
3314 }
3315 }
3316
3317 if (b.src === "play.svg" && !this.scroll)
3318 ctx.fillStyle = "#FFC0C0";
3319 else
3320 ctx.fillStyle = "#F0F0F0";
3321 ctx.strokeStyle = "#808080";
3322 ctx.fillRect(b.x1, b.y1, b.width, b.height);
3323 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
3324 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
3325
3326 y++;
3327 });
3328 }
3329
3330 // zoom buttons
3331 if (this.showZoomButtons) {
3332 let xb;
3333 if (this.showMenuButtons)
3334 xb = this.width - 26 - 40;
3335 else
3336 xb = this.width - 41;
3337 let yb = this.y1 - 20;
3338 ctx.fillStyle = "#F0F0F0";
3339 ctx.globalAlpha = 0.5;
3340 ctx.fillRect(xb, yb, 20, 20);
3341 ctx.globalAlpha = 1;
3342 ctx.strokeStyle = "#808080";
3343 ctx.strokeRect(xb, yb, 20, 20);
3344 ctx.strokeStyle = "#202020";
3345 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3346 ctx.drawLine(xb + 10, yb + 4, xb + 10, yb + 17);
3347
3348 xb += 20;
3349 ctx.globalAlpha = 0.5;
3350 ctx.fillRect(xb, yb, 20, 20);
3351 ctx.globalAlpha = 1;
3352 ctx.strokeStyle = "#808080";
3353 ctx.strokeRect(xb, yb, 20, 20);
3354 ctx.strokeStyle = "#202020";
3355 ctx.drawLine(xb + 4, yb + 10, xb + 17, yb + 10);
3356 }
3357
3358 // axis zoom
3359 if (this.zoom.x.active) {
3360 ctx.fillStyle = "#808080";
3361 ctx.globalAlpha = 0.2;
3362 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
3363 ctx.globalAlpha = 1;
3364 ctx.strokeStyle = "#808080";
3365 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
3366 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
3367 }
3368 if (this.zoom.y.active) {
3369 ctx.fillStyle = "#808080";
3370 ctx.globalAlpha = 0.2;
3371 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
3372 ctx.globalAlpha = 1;
3373 ctx.strokeStyle = "#808080";
3374 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
3375 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
3376 }
3377
3378 // marker
3379 if (this.marker.active) {
3380
3381 // round marker
3382 ctx.beginPath();
3383 ctx.globalAlpha = 0.1;
3384 ctx.arc(this.marker.x, this.marker.y, 10, 0, 2 * Math.PI);
3385 ctx.fillStyle = "#000000";
3386 ctx.fill();
3387 ctx.globalAlpha = 1;
3388
3389 ctx.beginPath();
3390 ctx.arc(this.marker.x, this.marker.y, 4, 0, 2 * Math.PI);
3391 ctx.fillStyle = "#000000";
3392 ctx.fill();
3393
3394 ctx.strokeStyle = "#A0A0A0";
3395 ctx.drawLine(this.marker.x, this.y1, this.marker.x, this.y2);
3396
3397 // text label
3398 let v = this.marker.v;
3399
3400 let s;
3401 if (this.param.Label[this.marker.graphIndex] !== "")
3402 s = this.param.Label[this.marker.graphIndex];
3403 else
3404 s = this.param["Variables"][this.marker.graphIndex];
3405
3406 if (this.param["Show raw value"] !== undefined &&
3407 this.param["Show raw value"][this.marker.graphIndex])
3408 s += " (Raw)";
3409
3410 s += ": " + v.toPrecision(this.yPrecision).stripZeros();
3411
3412 let w = ctx.measureText(s).width + 6;
3413 let h = ctx.measureText("M").width * 1.2 + 6;
3414 let x = this.marker.mx + 20;
3415 let y = this.marker.my + h / 3 * 2;
3416 let xl = x;
3417 let yl = y;
3418
3419 if (x + w >= this.x2) {
3420 x = this.marker.x - 20 - w;
3421 xl = x + w;
3422 }
3423
3424 if (y > (this.y1 - this.y2) / 2) {
3425 y = this.marker.y - h / 3 * 5;
3426 yl = y + h;
3427 }
3428
3429 ctx.strokeStyle = "#808080";
3430 ctx.fillStyle = "#F0F0F0";
3431 ctx.textBaseline = "middle";
3432 ctx.fillRect(x, y, w, h);
3433 ctx.strokeRect(x, y, w, h);
3434 ctx.fillStyle = "#404040";
3435 ctx.fillText(s, x + 3, y + h / 2);
3436
3437 // vertical line
3438 ctx.strokeStyle = "#808080";
3439 ctx.drawLine(this.marker.x, this.marker.y, xl, yl);
3440
3441 // time label
3442 s = timeToLabel(this.marker.t, 1, true);
3443 w = ctx.measureText(s).width + 10;
3444 h = ctx.measureText("M").width * 1.2 + 11;
3445 x = this.marker.x - w / 2;
3446 y = this.y1;
3447 if (x <= this.x1)
3448 x = this.x1;
3449 if (x + w >= this.x2)
3450 x = this.x2 - w;
3451
3452 ctx.strokeStyle = "#808080";
3453 ctx.fillStyle = "#F0F0F0";
3454 ctx.fillRect(x, y, w, h);
3455 ctx.strokeRect(x, y, w, h);
3456 ctx.fillStyle = "#404040";
3457 ctx.fillText(s, x + 5, y + h / 2);
3458 }
3459
3460 this.lastDrawTime = new Date().getTime();
3461
3462 // profile("Finished draw");
3463
3464 if (update_last_written) {
3465 this.updateLastWritten();
3466 }
3467
3468 // update URL
3469 if (this.updateURLTimer !== undefined)
3470 window.clearTimeout(this.updateURLTimer);
3471
3472 if (this.plotIndex === 0 && this.floating !== true)
3473 this.updateURLTimer = window.setTimeout(this.updateURL.bind(this), 10);
3474};
3475
3476MhistoryGraph.prototype.drawVAxis = function (ctx, x1, y1, height, minor, major,
3477 text, label, grid, ymin, ymax, logaxis, draw) {
3478 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
3479 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
3480 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
3481
3482 if (x1 > 0)
3483 ctx.textAlign = "right";
3484 else
3485 ctx.textAlign = "left";
3486 ctx.textBaseline = "middle";
3487 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
3488
3489 if (ymax <= ymin || height <= 0)
3490 return undefined;
3491
3492 if (!isFinite(ymax - ymin) || ymax == Number.MAX_VALUE) {
3493 dy = Number.MAX_VALUE / 10;
3494 label_dy = dy;
3495 major_dy = dy;
3496 n_sig1 = 1;
3497 } else if (logaxis) {
3498 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
3499 if (dy === 0) {
3500 ymin = 1E-20;
3501 dy = 1E-20;
3502 }
3503 label_dy = dy;
3504 major_dy = dy * 10;
3505 n_sig1 = 4;
3506 } else {
3507 // use 6 as min tick distance
3508 dy = (ymax - ymin) / (height / 6);
3509
3510 int_dy = Math.floor(Math.log(dy) / Math.log(10));
3511 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
3512
3513 if (frac_dy < 0) {
3514 frac_dy += 1;
3515 int_dy -= 1;
3516 }
3517
3518 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
3519 major_base = label_base = tick_base + 1;
3520
3521 // rounding up of dy, label_dy
3522 dy = Math.pow(10, int_dy) * base[tick_base];
3523 major_dy = Math.pow(10, int_dy) * base[major_base];
3524 label_dy = major_dy;
3525
3526 // number of significant digits
3527 if (ymin === 0)
3528 n_sig1 = 1;
3529 else
3530 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
3531 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3532
3533 if (ymax === 0)
3534 n_sig2 = 1;
3535 else
3536 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
3537 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
3538
3539 n_sig1 = Math.max(n_sig1, n_sig2);
3540 n_sig1 = Math.max(1, n_sig1);
3541
3542 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
3543 if (Math.abs(ymin) < 100000)
3544 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
3545 Math.log(10) + 0.001) + 1);
3546 if (Math.abs(ymax) < 100000)
3547 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
3548 Math.log(10) + 0.001) + 1);
3549
3550 // increase label_dy if labels would overlap
3551 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
3552 label_base++;
3553 label_dy = Math.pow(10, int_dy) * base[label_base];
3554 if (label_base % 3 === 2 && major_base % 3 === 1) {
3555 major_base++;
3556 major_dy = Math.pow(10, int_dy) * base[major_base];
3557 }
3558 }
3559 }
3560
3561 y_act = Math.floor(ymin / dy) * dy;
3562
3563 let last_label_y = y1;
3564 let maxwidth = 0;
3565
3566 if (draw) {
3567 ctx.strokeStyle = this.color.axis;
3568 ctx.drawLine(x1, y1, x1, y1 - height);
3569 }
3570
3571 do {
3572 if (logaxis)
3573 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
3574 (Math.log(ymax) - Math.log(ymin)) * height;
3575 else if (!(isFinite(ymax - ymin)))
3576 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
3577 else
3578 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
3579 ys = Math.round(y_screen);
3580
3581 if (y_screen < y1 - height - 0.001 || isNaN(ys))
3582 break;
3583
3584 if (y_screen <= y1 + 0.001) {
3585 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
3586 dy / major_dy / 10.0) {
3587
3588 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
3589 dy / label_dy / 10.0) {
3590 // label tick mark
3591 if (draw) {
3592 ctx.strokeStyle = this.color.axis;
3593 ctx.drawLine(x1, ys, x1 + text, ys);
3594 }
3595
3596 // grid line
3597 if (grid !== 0 && ys < y1 && ys > y1 - height)
3598 if (draw) {
3599 ctx.strokeStyle = this.color.grid;
3600 ctx.drawLine(x1, ys, x1 + grid, ys);
3601 }
3602
3603 // label
3604 if (label !== 0) {
3605 let str;
3606 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3607 str = y_act.toExponential(n_sig1).stripZeros();
3608 else
3609 str = y_act.toPrecision(n_sig1).stripZeros();
3610 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3611 if (draw) {
3612 ctx.strokeStyle = this.color.label;
3613 ctx.fillStyle = this.color.label;
3614 ctx.fillText(str, x1 + label, ys);
3615 }
3616 last_label_y = ys - textHeight / 2;
3617 }
3618 } else {
3619 // major tick mark
3620 if (draw) {
3621 ctx.strokeStyle = this.color.axis;
3622 ctx.drawLine(x1, ys, x1 + major, ys);
3623 }
3624
3625 // grid line
3626 if (grid !== 0 && ys < y1 && ys > y1 - height)
3627 if (draw) {
3628 ctx.strokeStyle = this.color.grid;
3629 ctx.drawLine(x1, ys, x1 + grid, ys);
3630 }
3631 }
3632
3633 if (logaxis) {
3634 dy *= 10;
3635 major_dy *= 10;
3636 label_dy *= 10;
3637 }
3638
3639 } else
3640 // minor tick mark
3641 if (draw) {
3642 ctx.strokeStyle = this.color.axis;
3643 ctx.drawLine(x1, ys, x1 + minor, ys);
3644 }
3645
3646 // for logaxis, also put labels on minor tick marks
3647 if (logaxis) {
3648 if (label !== 0) {
3649 let str;
3650 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
3651 str = y_act.toExponential(n_sig1).stripZeros();
3652 else
3653 str = y_act.toPrecision(n_sig1).stripZeros();
3654 if (ys - textHeight / 2 > y1 - height &&
3655 ys + textHeight / 2 < y1 &&
3656 ys + textHeight < last_label_y + 2) {
3657 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
3658 if (draw) {
3659 ctx.strokeStyle = this.color.label;
3660 ctx.fillStyle = this.color.label;
3661 ctx.fillText(str, x1 + label, ys);
3662 }
3663 }
3664
3665 last_label_y = ys;
3666 }
3667 }
3668 }
3669
3670 y_act += dy;
3671
3672 // suppress 1.23E-17 ...
3673 if (Math.abs(y_act) < dy / 100)
3674 y_act = 0;
3675
3676 } while (1);
3677
3678 return maxwidth;
3679};
3680
3681let options1 = {
3682 timeZone: 'UTC',
3683 day: '2-digit', month: 'short', year: '2-digit',
3684 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3685};
3686
3687let options2 = {
3688 timeZone: 'UTC',
3689 day: '2-digit', month: 'short', year: '2-digit',
3690 hour12: false, hour: '2-digit', minute: '2-digit'
3691};
3692
3693let options3 = {
3694 timeZone: 'UTC',
3695 day: '2-digit', month: 'short', year: '2-digit',
3696 hour12: false, hour: '2-digit', minute: '2-digit'
3697};
3698
3699let options4 = {
3700 timeZone: 'UTC',
3701 day: '2-digit', month: 'short', year: '2-digit'
3702};
3703
3704let options5 = {
3705 timeZone: 'UTC',
3706 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
3707};
3708
3709let options6 = {
3710 timeZone: 'UTC',
3711 hour12: false, hour: '2-digit', minute: '2-digit'
3712};
3713
3714let options7 = {
3715 timeZone: 'UTC',
3716 hour12: false, hour: '2-digit', minute: '2-digit'
3717};
3718
3719let options8 = {
3720 timeZone: 'UTC',
3721 day: '2-digit', month: 'short', year: '2-digit',
3722 hour12: false, hour: '2-digit', minute: '2-digit'
3723};
3724
3725let options9 = {
3726 timeZone: 'UTC',
3727 day: '2-digit', month: 'short', year: '2-digit'
3728};
3729
3730function timeToLabel(sec, base, forceDate) {
3731 let d = mhttpd_get_display_time(sec).date;
3732
3733 if (forceDate) {
3734 if (base < 60) {
3735 return d.toLocaleTimeString('en-GB', options1);
3736 } else if (base < 600) {
3737 return d.toLocaleTimeString('en-GB', options2);
3738 } else if (base < 3600 * 24) {
3739 return d.toLocaleTimeString('en-GB', options3);
3740 } else {
3741 return d.toLocaleDateString('en-GB', options4);
3742 }
3743 }
3744
3745 if (base < 60) {
3746 return d.toLocaleTimeString('en-GB', options5);
3747 } else if (base < 600) {
3748 return d.toLocaleTimeString('en-GB', options6);
3749 } else if (base < 3600 * 3) {
3750 return d.toLocaleTimeString('en-GB', options7);
3751 } else if (base < 3600 * 24) {
3752 return d.toLocaleTimeString('en-GB', options8);
3753 } else {
3754 return d.toLocaleDateString('en-GB', options9);
3755 }
3756}
3757
3758MhistoryGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
3759 text, label, grid, xmin, xmax) {
3760 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
3761 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
3762
3763 ctx.textAlign = "left";
3764 ctx.textBaseline = "top";
3765
3766 if (xmax <= xmin || width <= 0)
3767 return;
3768
3769 /* force date display if xmax not today */
3770 let d1 = new Date(xmax * 1000);
3771 let d2 = new Date();
3772 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
3773
3774 /* use 5 pixel as min tick distance */
3775 let dx = Math.round((xmax - xmin) / (width / 5));
3776
3777 let tick_base;
3778 for (tick_base = 0; base[tick_base]; tick_base++) {
3779 if (base[tick_base] > dx)
3780 break;
3781 }
3782 if (!base[tick_base])
3783 tick_base--;
3784 dx = base[tick_base];
3785
3786 let major_base = tick_base;
3787 let major_dx = dx;
3788
3789 let label_base = major_base;
3790 let label_dx = dx;
3791
3792 do {
3793 let str = timeToLabel(xmin, label_dx, forceDate);
3794 let maxwidth = ctx.measureText(str).width;
3795
3796 /* increasing label_dx, if labels would overlap */
3797 if (maxwidth > 0.75 * label_dx / (xmax - xmin) * width) {
3798 if (base[label_base + 1])
3799 label_dx = base[++label_base];
3800 else
3801 label_dx += 3600 * 24;
3802
3803 if (label_base > major_base + 1 || !base[label_base + 1]) {
3804 if (base[major_base + 1])
3805 major_dx = base[++major_base];
3806 else
3807 major_dx += 3600 * 24;
3808 }
3809
3810 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3811 if (base[tick_base + 1])
3812 dx = base[++tick_base];
3813 else
3814 dx += 3600 * 24;
3815 }
3816
3817 } else
3818 break;
3819 } while (1);
3820
3821 let d = new Date(xmin * 1000);
3822 let tz = d.getTimezoneOffset() * 60;
3823
3824 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3825
3826 ctx.strokeStyle = this.color.axis;
3827 ctx.drawLine(x1, y1, x1 + width, y1);
3828
3829 do {
3830 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3831
3832 if (xs > x1 + width + 0.001)
3833 break;
3834
3835 if (xs >= x1) {
3836 if ((x_act - tz) % major_dx === 0) {
3837 if ((x_act - tz) % label_dx === 0) {
3838 // label tick mark
3839 ctx.strokeStyle = this.color.axis;
3840 ctx.drawLine(xs, y1, xs, y1 + text);
3841
3842 // grid line
3843 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3844 ctx.strokeStyle = this.color.grid;
3845 ctx.drawLine(xs, y1, xs, y1 + grid);
3846 }
3847
3848 // label
3849 if (label !== 0) {
3850 let str = timeToLabel(x_act, label_dx, forceDate);
3851
3852 // if labels at edge, shift them in
3853 let xl = xs - ctx.measureText(str).width / 2;
3854 if (xl < 0)
3855 xl = 0;
3856 if (xl + ctx.measureText(str).width >= xr)
3857 xl = xr - ctx.measureText(str).width - 1;
3858 ctx.strokeStyle = this.color.label;
3859 ctx.fillStyle = this.color.label;
3860 ctx.fillText(str, xl, y1 + label);
3861 }
3862 } else {
3863 // major tick mark
3864 ctx.strokeStyle = this.color.axis;
3865 ctx.drawLine(xs, y1, xs, y1 + major);
3866 }
3867
3868 // grid line
3869 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3870 ctx.strokeStyle = this.color.grid;
3871 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
3872 }
3873 } else {
3874 // minor tick mark
3875 ctx.strokeStyle = this.color.axis;
3876 ctx.drawLine(xs, y1, xs, y1 + minor);
3877 }
3878 }
3879
3880 x_act += dx;
3881
3882 } while (1);
3883};
3884
3885MhistoryGraph.prototype.download = function (mode) {
3886
3887 let leftDate = mhttpd_get_display_time(this.tMin).date;
3888 let rightDate = mhttpd_get_display_time(this.tMax).date;
3889 let filename = this.group + "-" + this.panel + "-" +
3890 leftDate.getFullYear() +
3891 ("0" + (leftDate.getUTCMonth() + 1)).slice(-2) +
3892 ("0" + leftDate.getUTCDate()).slice(-2) + "-" +
3893 ("0" + leftDate.getUTCHours()).slice(-2) +
3894 ("0" + leftDate.getUTCMinutes()).slice(-2) +
3895 ("0" + leftDate.getUTCSeconds()).slice(-2) + "-" +
3896 rightDate.getFullYear() +
3897 ("0" + (rightDate.getUTCMonth() + 1)).slice(-2) +
3898 ("0" + rightDate.getUTCDate()).slice(-2) + "-" +
3899 ("0" + rightDate.getUTCHours()).slice(-2) +
3900 ("0" + rightDate.getUTCMinutes()).slice(-2) +
3901 ("0" + rightDate.getUTCSeconds()).slice(-2);
3902
3903 // use trick from FileSaver.js
3904 let a = document.getElementById('downloadHook');
3905 if (a === null) {
3906 a = document.createElement("a");
3907 a.style.display = "none";
3908 a.id = "downloadHook";
3909 document.body.appendChild(a);
3910 }
3911
3912 if (mode === "CSV") {
3913 filename += ".csv";
3914
3915 let data = "";
3916 this.param["Variables"].forEach(v => {
3917 data += "Time,";
3918 if (this.binned)
3919 data += v + " MIN," + v + " MAX,";
3920 else
3921 data += v + ",";
3922 });
3923 data = data.slice(0, -1);
3924 data += '\n';
3925
3926 let maxlen = 0;
3927 let nvar = this.param["Variables"].length;
3928 for (let index=0 ; index < nvar ; index++)
3929 if (this.data[index].time.length > maxlen)
3930 maxlen = this.data[index].time.length;
3931 let index = [];
3932 for (let di=0 ; di < nvar ; di++)
3933 for (let i = 0; i < maxlen; i++) {
3934 if (i < this.data[di].time.length &&
3935 this.data[di].time[i] > this.tMin) {
3936 index[di] = i;
3937 break;
3938 }
3939 }
3940
3941 for (let i = 0; i < maxlen; i++) {
3942 let l = "";
3943 for (let di = 0 ; di < nvar ; di++) {
3944 if (index[di] < this.data[di].time.length &&
3945 this.data[di].time[index[di]] > this.tMin && this.data[di].time[index[di]] < this.tMax) {
3946 if (this.binned) {
3947 l += this.data[di].time[index[di]] + ",";
3948
3949 if (this.param["Show raw value"] !== undefined &&
3950 this.param["Show raw value"][di]) {
3951 l += this.data[di].binRaw[index[di]].minValue + ",";
3952 l += this.data[di].binRaw[index[di]].maxValue + ",";
3953 } else {
3954 l += this.data[di].bin[index[di]].minValue + ",";
3955 l += this.data[di].bin[index[di]].maxValue + ",";
3956 }
3957
3958 } else {
3959
3960 l += this.data[di].time[index[di]] + ",";
3961
3962 if (this.param["Show raw value"] !== undefined &&
3963 this.param["Show raw value"][di])
3964 l += this.data[di].rawValue[index[di]] + ",";
3965 else
3966 l += this.data[di].value[index[di]] + ",";
3967 }
3968 } else {
3969 l += ",,";
3970 }
3971 index[di]++;
3972 }
3973 if (l.split(',').some(s => s)) { // don't add if only commas
3974 l = l.slice(0, -1); // remove last comma
3975 data += l + '\n';
3976 }
3977 }
3978
3979 let blob = new Blob([data], {type: "text/csv"});
3980 let url = window.URL.createObjectURL(blob);
3981
3982 a.href = url;
3983 a.download = filename;
3984 a.click();
3985 window.URL.revokeObjectURL(url);
3986 dlgAlert("Data downloaded to '" + filename + "'");
3987
3988 } else if (mode === "PNG") {
3989 filename += ".png";
3990
3991 this.showZoomButtons = false;
3992 this.showMenuButtons = false;
3993 this.forceRedraw = true;
3994 this.forceConvert = true;
3995 this.draw();
3996
3997 let h = this;
3998 this.canvas.toBlob(function (blob) {
3999 let url = window.URL.createObjectURL(blob);
4000
4001 a.href = url;
4002 a.download = filename;
4003 a.click();
4004 window.URL.revokeObjectURL(url);
4005 dlgAlert("Image downloaded to '" + filename + "'");
4006
4007 h.showZoomButtons = true;
4008 h.showMenuButtons = true;
4009 h.forceRedraw = true;
4010 h.forceConvert = true;
4011 h.draw();
4012
4013 }, 'image/png');
4014 } else if (mode === "URL") {
4015 // Create new element
4016 let el = document.createElement('textarea');
4017
4018 // Set value (string to be copied)
4019 let url = this.baseURL + "&group=" + this.group + "&panel=" + this.panel +
4020 "&A=" + this.tMin + "&B=" + this.tMax;
4021 url = encodeURI(url);
4022 el.value = url;
4023
4024 // Set non-editable to avoid focus and move outside of view
4025 el.setAttribute('readonly', '');
4026 el.style = {position: 'absolute', left: '-9999px'};
4027 document.body.appendChild(el);
4028 // Select text inside element
4029 el.select();
4030 // Copy text to clipboard
4031 document.execCommand('copy');
4032 // Remove temporary element
4033 document.body.removeChild(el);
4034
4035 dlgMessage("Info", "URL<br/><br/>" + url + "<br/><br/>copied to clipboard", true, false);
4036 }
4037
4038};