MIDAS
Loading...
Searching...
No Matches
mplot.js
Go to the documentation of this file.
1//
2// Name: mplot.js
3// Created by: Stefan Ritt
4//
5// Contents: JavaScript graph plotting routines
6//
7// Note: please load midas.js, mhttpd.js and control.js before mplot.js
8//
9
10let defaultParam = {
11
12 showMenuButtons: true,
13
14 color: {
15 background: "#FFFFFF",
16 axis: "#808080",
17 grid: "#D0D0D0",
18 label: "#404040",
19 data: [
20 "#00AAFF", "#FF9000", "#FF00A0", "#00C030",
21 "#A0C0D0", "#D0A060", "#C04010", "#807060",
22 "#F0C000", "#2090A0", "#D040D0", "#90B000",
23 "#B0B040", "#B0B0FF", "#FFA0A0", "#A0FFA0"],
24 },
25
26 title: {
27 color: "#000000",
28 backgroundColor: "#808080",
29 textSize: 24,
30 text: ""
31 },
32
33 legend: {
34 show: true,
35 color: "#D0D0D0",
36 backgroundColor: "#FFFFFF",
37 textColor: "#404040",
38 textSize: 16
39 },
40
41 xAxis: {
42 type: "numeric", // One of "numeric", "datetime", "category"
43 log: false,
44 min: undefined,
45 max: undefined,
46 grid: true,
47 textSize: 20,
48 title: {
49 text: "",
50 textSize : 20
51 }
52 },
53
54 yAxis: {
55 log: false,
56 min: undefined,
57 max: undefined,
58 grid: true,
59 textSize: 20,
60 title: {
61 text: "",
62 textSize : 20
63 }
64 },
65
66 zAxis: {
67 show: true,
68 min: undefined,
69 max: undefined,
70 textSize: 14,
71 title: {
72 text: "",
73 textSize : 20
74 }
75 },
76
77 stats: {
78 show: true
79 },
80
81 plot: [
82 {
83 type: "scatter", // One of "scatter", "histogram", "colormap"
84 odbPath: "",
85 x: "",
86 y: "",
87 label: "",
88 alpha: 1,
89 barWidth: 0.3,
90
91 marker: {
92 draw: true,
93 lineColor: 0,
94 fillColor: 0,
95 style: "circle", // One of "none", "circle", "square", "diamond", "pentagon", "triangle-up", "triangle-down", "triangle-left", "triangle-right", "cross", "plus"
96 size: 10,
97 lineWidth: 2
98 },
99
100 line: {
101 draw: true,
102 fill: false,
103 color: 0,
104 style: "solid", // One of "none", "solid"
105 width: 2
106 }
107 },
108 ],
109};
110
111function mplot_init() {
112 /**
113 * Initialize <div> objects of class mplot
114 * Can only be called after all data has been created
115 */
116
117 // go through all data-name="mplot" tags
118 let mPlot = document.getElementsByClassName("mplot");
119
120 for (let i = 0; i < mPlot.length; i++)
121 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
122
123 loadMPlotData();
124
125 window.addEventListener('resize', windowResize);
126}
127
128function profile(flag) {
129 if (flag === true || flag === undefined) {
130 console.log("");
131 profile.startTime = new Date().getTime();
132 return;
133 }
134
135 let now = new Date().getTime();
136 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
137 profile.startTime = new Date().getTime();
138}
139
140function windowResize() {
141 /**
142 * Resize all mplot objects as defined by their class
143 */
144 let mPlot = document.getElementsByClassName("mplot");
145 for (const m of mPlot)
146 m.mpg.resize();
147}
148
149function isObject(item) {
150 return (item && typeof item === 'object' && !Array.isArray(item));
151}
152
153function deepMerge(target, source) {
154 /**
155 * Recursively merge object keys from source into target. Modifies in-place.
156 * @param {object} target the object to make new keys or overwrite existing ones
157 * @param {object} source the object from which the keys are copied
158 * @returns {object} the target with the new keys
159 */
160 for (let key in source) {
161 if (source.hasOwnProperty(key)) {
162 if (isObject(source[key])) {
163 if (!target[key]) Object.assign(target, { [key]: {} });
164 deepMerge(target[key], source[key]);
165 } else {
166 Object.assign(target, { [key]: source[key] });
167 }
168 }
169 }
170 return target;
171}
172
173function MPlotGraph(divElement, param) { // Constructor
174 /**
175 * MPlotGraph Constructor
176 * @param {div element} divElement HTML <div> element to place the plot into
177 @param {object} param JS object with keys a subset of DefaultParam, controls plot display style
178 */
179
180 // save parameters from <div>
181 this.parentDiv = divElement;
182 this.divParam = divElement.innerHTML;
183 divElement.innerHTML = "";
184
185 // if absent, generate random string (5 char) to give an id to parent element
186 if (!this.parentDiv.id)
187 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
188
189 // default parameters
190 this.param = JSON.parse(JSON.stringify(defaultParam)); // deep copy
191
192 // overwrite default parameters from <div> text body
193 try {
194 if (this.divParam.includes('{')) {
195 let p = JSON.parse(this.divParam);
196 this.param = deepMerge(this.param, p);
197 }
198 } catch (error) {
199 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
200 dlgAlert(error);
201 return;
202 }
203
204 // obtain parameters form <div> attributes ---
205
206 // data-odb-path
207 if (this.parentDiv.dataset.odbPath)
208 for (let p of this.param.plot)
209 p.odbPath = this.parentDiv.dataset.odbPath;
210
211 // data-type
212 if (this.parentDiv.dataset.type)
213 this.param.type = this.parentDiv.dataset.type;
214
215 // data-title
216 if (this.parentDiv.dataset.title)
217 this.param.title.text = this.parentDiv.dataset.title;
218
219 // data-x/y/z-text
220 if (this.parentDiv.dataset.xText)
221 this.param.xAxis.title.text =this.parentDiv.dataset.xText;
222 if (this.parentDiv.dataset.yText)
223 this.param.yAxis.title.text =this.parentDiv.dataset.yText;
224 if (this.parentDiv.dataset.zText)
225 this.param.zAxis.title.text =this.parentDiv.dataset.zText;
226
227 // data-x/y
228 if (this.parentDiv.dataset.x)
229 this.param.plot[0].x = this.parentDiv.dataset.x;
230 if (this.parentDiv.dataset.y)
231 this.param.plot[0].y = this.parentDiv.dataset.y;
232
233 // data-x/y-error
234 if (this.parentDiv.dataset.xError)
235 this.param.plot[0].xError = this.parentDiv.dataset.xError;
236 if (this.parentDiv.dataset.yError)
237 this.param.plot[0].yError = this.parentDiv.dataset.yError;
238
239 // data-x/y/z-min/max
240 if (this.parentDiv.dataset.xMin)
241 this.param.xAxis.min = parseFloat(this.parentDiv.dataset.xMin);
242 if (this.parentDiv.dataset.xMax)
243 this.param.xAxis.max = parseFloat(this.parentDiv.dataset.xMax);
244 if (this.parentDiv.dataset.yMin)
245 this.param.yAxis.min = parseFloat(this.parentDiv.dataset.yMin);
246 if (this.parentDiv.dataset.yMax)
247 this.param.yAxis.max = parseFloat(this.parentDiv.dataset.yMax);
248 if (this.parentDiv.dataset.zMin)
249 this.param.zAxis.min = parseFloat(this.parentDiv.dataset.zMin);
250 if (this.parentDiv.dataset.zMax)
251 this.param.zAxis.max = parseFloat(this.parentDiv.dataset.zMax);
252
253 // data-x/y/z-log
254 if (this.parentDiv.dataset.xLog)
255 this.param.xAxis.log = this.parentDiv.dataset.xLog === "true" || this.parentDiv.dataset.xLog === "1";
256 if (this.parentDiv.dataset.yLog)
257 this.param.yAxis.log = this.parentDiv.dataset.yLog === "true" || this.parentDiv.dataset.yLog === "1";
258 if (this.parentDiv.dataset.zLog) {
259 this.param.zAxis.log = this.parentDiv.dataset.zLog === "true" || this.parentDiv.dataset.zLog === "1";
260 if (this.param.zAxis.log) {
261 if (this.param.zAxis.min < 1E-20)
262 this.param.zAxis.min = 1E-20;
263 if (this.param.zAxis.max < 1E-18)
264 this.param.zAxis.max = 1E-18;
265 }
266 }
267
268 // data-h
269 if (this.parentDiv.dataset.h) {
270 this.param.plot[0].type = "histogram";
271 this.param.plot[0].y = this.parentDiv.dataset.h;
272 this.param.plot[0].line.color = "#404040";
273 if (!this.parentDiv.dataset.x) {
274 this.param.plot[0].xMin = this.param.xAxis.min;
275 this.param.plot[0].xMax = this.param.xAxis.max;
276 }
277 }
278
279 // data-z
280 if (this.parentDiv.dataset.z) {
281 this.param.plot[0].type = "colormap";
282 this.param.plot[0].showZScale = true;
283 this.param.plot[0].bgcolor = this.parentDiv.dataset.bgcolor;
284 this.param.plot[0].z = this.parentDiv.dataset.z;
285 this.param.plot[0].xMin = this.param.xAxis.min;
286 this.param.plot[0].xMax = this.param.xAxis.max;
287 this.param.plot[0].yMin = this.param.yAxis.min;
288 this.param.plot[0].yMax = this.param.yAxis.max;
289 this.param.plot[0].zMin = this.param.zAxis.min;
290 this.param.plot[0].zMax = this.param.zAxis.max;
291 this.param.plot[0].nx = parseInt(this.parentDiv.dataset.nx);
292 this.param.plot[0].ny = parseInt(this.parentDiv.dataset.ny);
293 if (this.param.plot[0].nx === undefined) {
294 dlgAlert("\"data-nx\" missing for colormap mplot <div>");
295 return;
296 }
297 if (this.param.plot[0].ny === undefined) {
298 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
299 return;
300 }
301 }
302
303 // data-line-width
304 if (this.parentDiv.dataset["line-width"])
305 this.param.plot[0].line.width = this.parentDiv.dataset["line-width"];
306
307 // data-bar-width
308 if (this.parentDiv.dataset["bar-width"])
309 this.param.plot[0].barWidth = this.parentDiv.dataset["bar-width"];
310
311 // data-marker
312 if (this.parentDiv.dataset["marker-style"])
313 this.param.plot[0].marker.style = this.parentDiv.dataset["marker-style"];
314
315 // data-x<n>/y<n>/label<n>/alpha<n>/marker<n>
316 for (let i=0 ; i<16 ; i++) {
317 let index = 0;
318 if (this.parentDiv.dataset["x"+i]) {
319 if (this.param.plot[0].x !== "") {
320 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
321 index = this.param.plot.length-1;
322 this.param.plot[index].marker.lineColor = index;
323 this.param.plot[index].marker.fillColor = index;
324 this.param.plot[index].line.color = index;
325 }
326 this.param.plot[index].x = this.parentDiv.dataset["x" + i];
327 }
328 if (this.parentDiv.dataset["y"+i])
329 this.param.plot[index].y = this.parentDiv.dataset["y"+i];
330 if (this.parentDiv.dataset["x"+i+"Error"])
331 this.param.plot[index].xError = this.parentDiv.dataset["x"+i+"Error"];
332 if (this.parentDiv.dataset["y"+i+"Error"])
333 this.param.plot[index].yError = this.parentDiv.dataset["y"+i+"Error"];
334 if (this.parentDiv.dataset["label"+i])
335 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
336 if (this.parentDiv.dataset["alpha"+i])
337 this.param.plot[index].alpha = parseFloat(this.parentDiv.dataset["alpha"+i]);
338 if (this.parentDiv.dataset["line"+i+"-width"])
339 this.param.plot[index].line.width = parseFloat(this.parentDiv.dataset["line"+i+"-width"]);
340 if (this.parentDiv.dataset["marker"+i+"-style"])
341 this.param.plot[index].marker.style = this.parentDiv.dataset["marker"+i+"-style"];
342 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
343 }
344
345 // data-h<n>
346 for (let i=0 ; i<16 ; i++) {
347 let index = 0;
348 if (this.parentDiv.dataset["h"+i]) {
349 if (this.param.plot[0].y !== "") {
350 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
351 index = this.param.plot.length-1;
352 this.param.plot[index].marker.lineColor = index;
353 this.param.plot[index].marker.fillColor = index;
354 this.param.plot[index].line.color = index;
355 }
356 this.param.plot[index].type = "histogram";
357 this.param.plot[index].y = this.parentDiv.dataset["h" + i];
358
359 this.param.plot[index].xMin = this.param.xAxis.min;
360 this.param.plot[index].xMax = this.param.xAxis.max;
361
362 if (this.parentDiv.dataset["label"+i])
363 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
364 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
365 }
366 }
367
368 // data-xaxis-type
369 if (this.parentDiv.dataset["x-type"])
370 this.param.xAxis.type = this.parentDiv.dataset["x-type"];
371
372 // data-overlay
373 if (this.parentDiv.dataset.overlay) {
374 this.param.overlay = this.parentDiv.dataset.overlay;
375 if (this.param.overlay.indexOf('(') !== -1) // strip any '('
376 this.param.overlay = this.param.overlay.substring(0, this.param.overlay.indexOf('('));
377 }
378
379 // data-event
380 if (this.parentDiv.dataset.event) {
381 this.param.event = this.parentDiv.dataset.event;
382 if (this.param.event.indexOf('(') !== -1) // strip any '('
383 this.param.event = this.param.event.substring(0, this.param.event.indexOf('('));
384 }
385
386 // data-stats
387 if (this.parentDiv.dataset.stats)
388 this.param.stats.show = (this.parentDiv.dataset.stats === "1");
389
390 // set parameters from constructor
391 if (param) {
392 deepMerge(this.param, param);
393 if (this.param.plot[0].type === "colormap") {
394 this.calcMinMax();
395
396 if (this.param.plot[0].nx === undefined) {
397 dlgAlert("\"nx\" missing in param for colormap mplot <div>");
398 return;
399 }
400 if (this.param.plot[0].ny === undefined) {
401 dlgAlert("\"ny\" missing in param for colormap mplot <div>");
402 return;
403 }
404 }
405 }
406
407 // dragging
408 this.drag = {
409 active: false,
410 sxStart: 0,
411 syStart: 0,
412 xStart: 0,
413 yStart: 0,
414 xMinStart: 0,
415 xMaxStart: 0,
416 yMinStart: 0,
417 yMaxStart: 0,
418 };
419
420 // axis zoom
421 this.zoom = {
422 x: {active: false},
423 y: {active: false}
424 };
425
426 // marker
427 this.marker = {active: false};
428 this.blockAutoScale = false;
429
430 this.error = null;
431
432 // buttons
433 this.button = [
434 {
435 src: "menu.svg",
436 title: "Show / hide legend",
437 click: function (t) {
438 t.param.legend.show = !t.param.legend.show;
439 t.redraw();
440 }
441 },
442 {
443 src: "stats.svg",
444 title: "Show / hide statistics",
445 click: function (t) {
446 t.param.stats.show = !t.param.stats.show;
447 t.redraw();
448 }
449 },
450 {
451 src: "rotate-ccw.svg",
452 title: "Reset histogram axes",
453 click: function (t) {
454 t.resetAxes();
455 }
456 },
457 {
458 src: "download.svg",
459 title: "Download image/data...",
460 click: function (t) {
461 if (t.downloadSelector.style.display === "none") {
462 t.downloadSelector.style.display = "block";
463 let w = t.downloadSelector.getBoundingClientRect().width;
464 t.downloadSelector.style.left = (t.canvas.getBoundingClientRect().x + window.scrollX +
465 t.width - 26 - w) + "px";
466 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.scrollY +
467 this.y1) + "px";
468 t.downloadSelector.style.zIndex = "32";
469 } else {
470 t.downloadSelector.style.display = "none";
471 }
472 }
473 },
474 ];
475
476 this.button.forEach(b => {
477 b.img = new Image();
478 b.img.src = "icons/" + b.src;
479 });
480
481 this.createDownloadSelector();
482
483 // mouse event handlers
484 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
485 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
486 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
487 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
488 divElement.addEventListener("wheel", this.mouseEvent.bind(this), true);
489
490 // Keyboard event handler (has to be on the window!)
491 window.addEventListener("keydown", this.keyDown.bind(this));
492
493 // create canvas
494 this.canvas = document.createElement("canvas");
495 this.canvas.style.border = "1px solid black";
496
497 if (parseInt(this.parentDiv.style.width) > 0)
498 this.canvas.width = parseInt(this.parentDiv.style.width);
499 else
500 this.canvas.width = 500;
501 if (parseInt(this.parentDiv.style.height) > 0)
502 this.canvas.height = parseInt(this.parentDiv.style.height);
503 else
504 this.canvas.height = 300;
505
506 divElement.appendChild(this.canvas);
507}
508
509MPlotGraph.prototype.createDownloadSelector = function () {
510 /**
511 * Make a download section menu
512 */
513
514 // download selector
515 let downloadSelId = this.parentDiv.id + "downloadSel";
516 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
517 this.downloadSelector = document.createElement("div");
518 this.downloadSelector.id = downloadSelId;
519 this.downloadSelector.style.display = "none";
520 this.downloadSelector.style.position = "absolute";
521 this.downloadSelector.className = "mtable";
522 this.downloadSelector.style.borderRadius = "0";
523 this.downloadSelector.style.border = "2px solid #808080";
524 this.downloadSelector.style.margin = "0";
525 this.downloadSelector.style.padding = "0";
526
527 this.downloadSelector.style.left = "100px";
528 this.downloadSelector.style.top = "100px";
529
530 let table = document.createElement("table");
531 let mhg = this;
532
533 let row = document.createElement("tr");
534 let cell = document.createElement("td");
535 cell.style.padding = "0";
536 let link = document.createElement("a");
537 link.href = "#";
538 link.innerHTML = "CSV";
539 link.title = "Download data in Comma Separated Value format";
540 link.onclick = function () {
541 mhg.downloadSelector.style.display = "none";
542 mhg.download("CSV");
543 return false;
544 }.bind(this);
545 cell.appendChild(link);
546 row.appendChild(cell);
547 table.appendChild(row);
548
549 row = document.createElement("tr");
550 cell = document.createElement("td");
551 cell.style.padding = "0";
552 link = document.createElement("a");
553 link.href = "#";
554 link.innerHTML = "PNG";
555 link.title = "Download image in PNG format";
556 link.onclick = function () {
557 mhg.downloadSelector.style.display = "none";
558 mhg.download("PNG");
559 return false;
560 }.bind(this);
561 cell.appendChild(link);
562 row.appendChild(cell);
563 table.appendChild(row);
564
565 this.downloadSelector.appendChild(table);
566 document.body.appendChild(this.downloadSelector);
567}
568
569MPlotGraph.prototype.keyDown = function (e) {
570 /**
571 * Handle key events
572 * @param {Object} e keydown event with properties key, metaKey, ctrlKey, target, etc
573 */
574 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
575
576 // don't grab key if we are in an input field
577 if (e.target.tagName === "INPUT")
578 return;
579
580 this.resetAxes();
581 e.preventDefault();
582 }
583}
584
585function loadMPlotData() {
586 /**
587 * Load data from the ODB for all HTML elements with the mplot class
588 */
589
590 // go through all data-name="mplot" tags
591 let mPlot = document.getElementsByClassName("mplot");
592
593 let v = [];
594 for (const mp of mPlot) {
595 for (const pl of mp.mpg.param.plot) {
596 if (pl.odbPath === undefined || pl.odbPath === "")
597 continue;
598
599 let name = pl.label;
600 if (name === "")
601 name = mp.id;
602
603 if ((pl.type === "scatter" || pl.type === "histogram") &&
604 (pl.y === undefined || pl.y === null || pl.y === "")) {
605 mp.mpg.error ="Invalid Y data \"" + pl.y + "\" for " + pl.type + " plot \"" + name+ "\"";
606 mp.mpg.draw();
607 pl.invalid = true;
608 continue;
609 }
610
611 if ((pl.type === "colormap") &&
612 (pl.z === undefined || pl.z === null || pl.z === "")) {
613 mp.mpg.error = "Invalid Z data \"" + pl.y + "\" for colormap plot \"" + name + "\"";
614 mp.mpg.draw();
615 pl.invalid = true;
616 continue;
617 }
618
619 if (pl.odbPath.slice(-1) !== '/')
620 pl.odbPath += '/';
621
622 if (pl.x !== undefined && pl.x !== null && pl.x !== "")
623 v.push(pl.odbPath + pl.x);
624 if (pl.y !== undefined && pl.y !== null && pl.y !== "")
625 v.push(pl.odbPath + pl.y);
626 if (pl.z !== undefined && pl.z !== null && pl.z !== "")
627 v.push(pl.odbPath + pl.z);
628 if (pl.xError !== undefined && pl.xError !== null && pl.xError !== "")
629 v.push(pl.odbPath + pl.xError);
630 if (pl.yError !== undefined && pl.yError !== null && pl.yError !== "")
631 v.push(pl.odbPath + pl.yError);
632 }
633 }
634
635 mjsonrpc_db_get_values(v).then( function(rpc) {
636
637 let mPlot = document.getElementsByClassName("mplot");
638 let i = 0;
639 let iPlot = 0;
640 for (let mp of mPlot) {
641 for (let p of mp.mpg.param.plot) {
642 if (!p.odbPath === undefined || p.odbPath === "" || p.invalid)
643 continue;
644
645 let name = p.label;
646 if (name === "")
647 name = mp.id;
648
649 if (p.x !== undefined && p.x !== null && p.x !== "") {
650 p.xData = rpc.result.data[i++];
651 if (p.xData === null)
652 mp.mpg.error = "Invalid X data \"" + p.x + "\" for plot \"" + name + "\"";
653 if (Array.isArray(p.xData) && p.xData.length > 0 &&
654 p.xData.every(item => typeof item === "string")) {
655
656 // switch plot to category plot if sting array found for x-data
657 p.type = "category";
658 p.marker = undefined;
659 mp.mpg.param.xAxis.type = "category";
660 mp.mpg.param.stats.show = false;
661 }
662 }
663 if (p.y !== undefined && p.y !== null && p.y !== "") {
664 p.yData = rpc.result.data[i++];
665 if (p.yData === null)
666 mp.mpg.error = "Invalid Y data \"" + p.y + "\" for plot \"" + name + "\"";
667 }
668 if (p.z !== undefined && p.z !== null && p.z !== "") {
669 p.zData = rpc.result.data[i++];
670 if (p.zData === null)
671 mp.mpg.error = "Invalid Z data \"" + p.z + "\" for plot \"" + name + "\"";
672 }
673 if (p.xError !== undefined && p.xError !== null && p.xError !== "") {
674 p.xErrorData = rpc.result.data[i++];
675 if (p.xErrorData === null)
676 mp.mpg.error = "Invalid X error data \"" + p.xError + "\" for plot \"" + name + "\"";
677 }
678 if (p.yError !== undefined && p.yError !== null && p.yError !== "") {
679 p.yErrorData = rpc.result.data[i++];
680 if (p.yErrorData === null)
681 mp.mpg.error = "Invalid Y error data \"" + p.yError + "\" for plot \"" + name + "\"";
682 }
683
684 if ((p.type === "scatter" || p.type === "histogram" || p.type === "category") &&
685 mp.mpg.error === null) {
686 // generate X data for histograms
687 if (p.xData === undefined || p.xData === null) {
688
689 if (p.type === "scatter") {
690 // scatter plot goes from 0 ... N
691 p.xMin = 0;
692 p.xMax = p.yData.length;
693 p.xData = Array.from({length: p.yData.length}, (v, i) => i);
694 } else {
695 // histogram goes from -0.5 ... N-0.5 to have bins centered over bin x-value
696 p.xMin = -0.5;
697 p.xMax = p.yData.length - 0.5;
698
699 let dx = (p.xMax - p.xMin) / p.yData.length;
700 let x0 = p.xMin + dx / 2;
701 p.xData = Array.from({length: p.yData.length}, (v, i) => x0 + i * dx);
702 }
703 } else {
704 if (p.type === "category") {
705 p.xMin = 0;
706 p.xMax = p.xData.length;
707 } else if (p.xMin === undefined) {
708 p.xMin = Math.min(...p.xData);
709 p.xMax = Math.max(...p.xData);
710 }
711 }
712
713 p.yMin = Math.min(...p.yData);
714 p.yMax = Math.max(...p.yData);
715
716 if (p.type === "category")
717 p.yMin = 0;
718 }
719
720 if (p.type === "colormap" && mp.mpg.error === null) {
721 p.zMin = Math.min(...p.zData.filter(v=>!isNaN(v)));
722 p.zMax = Math.max(...p.zData.filter(v=>!isNaN(v)));
723
724 if (p.xMin === undefined) {
725 p.xMin = -0.5;
726 p.xMax = p.nx - 0.5;
727 }
728 if (p.yMin === undefined) {
729 p.yMin = -0.5;
730 p.yMax = p.ny - 0.5;
731 }
732
733 let dx = (p.xMax - p.xMin) / p.nx;
734 let x0 = p.xMin + dx/2;
735 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
736
737 let dy = (p.yMax - p.yMin) / p.ny;
738 let y0 = p.yMin + dy/2;
739 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
740 }
741
742 iPlot++;
743 }
744 }
745
746 for (const mp of mPlot) {
747 if (!mp.mpg.blockAutoScale)
748 mp.mpg.calcMinMax();
749 mp.mpg.redraw();
750 }
751
752 // refresh data once per second
753 window.setTimeout(loadMPlotData, 1000);
754
755 }).catch( (error) => {
756 dlgAlert(error)
757 });
758}
759
760MPlotGraph.prototype.setData = function (index, x, y) {
761 /**
762 * Set the data for the mplot
763 * @param {int} index Choose which data series to edit or add. Allows multiple data sets per plot
764 * @param {array} x values for the x axis
765 * @param {array} y values for the y axis
766 * @returns none
767 */
768 if (index > this.param.plot.length) {
769 dlgAlert("Wrong index \"" + index + "\" for graph \""+ this.param.title.text +"\"<br />" +
770 "New index must be \"" + this.param.plot.length + "\"");
771 return;
772 }
773
774 let p;
775
776 if (index + 1 > this.param.plot.length) {
777 // add new default plot
778 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
779 p = this.param.plot[index];
780 p.marker.lineColor = index;
781 p.marker.fillColor = index;
782 p.line.color = index;
783 p.type = y ? "scatter" : "histogram";
784 } else
785 p = this.param.plot[index];
786
787 p.odbPath = ""; // prevent loading of ODB data
788
789 if (p.type === "colormap") {
790 p.zData = x; // 2D array of colormap plot
791
792 p.zMin = undefined;
793 p.zMax = undefined;
794 for (const value of p.zData) {
795 if (!isNaN(value)) {
796 if (typeof p.zMin === 'undefined' || p.zMin > value)
797 p.zMin = value;
798 if (typeof p.zMax === 'undefined' || p.zMax < value)
799 p.zMax = value;
800 }
801 }
802
803 if (p.xMin === undefined) {
804 p.xMin = -0.5;
805 p.xMax = p.nx - 0.5;
806 }
807 if (p.yMin === undefined) {
808 p.yMin = -0.5;
809 p.yMax = p.ny - 0.5;
810 }
811
812 let dx = (p.xMax - p.xMin) / p.nx;
813 let x0 = p.xMin + dx/2;
814 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
815
816 let dy = (p.yMax - p.yMin) / p.ny;
817 let y0 = p.yMin + dy/2;
818 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
819 }
820
821
822 if (p.type === "histogram") {
823 p.yData = x;
824 p.line.color = "#404040";
825 // generate X data for histograms
826 if (p.xMin === undefined || p.xMax === undefined) {
827 p.xMin = -0.5;
828 p.xMax = p.yData.length - 0.5;
829 }
830 let dx = (p.xMax - p.xMin) / p.yData.length;
831 let x0 = p.xMin + dx/2;
832 if (p.xData === undefined || p.xData === null)
833 p.xData = Array.from({length: p.yData.length}, (v,i) => x0 + i*dx);
834
835 p.yMin = Math.min(...p.yData);
836 p.yMax = Math.max(...p.yData);
837 }
838
839 if (p.type === "category") {
840 p.xData = x;
841 p.yData = y;
842
843 p.xMin = 0;
844 p.xMax = x.length;
845 p.yMin = 0;
846 p.yMax = Math.max(...p.yData);
847 }
848
849 if (p.type === "scatter" ) {
850 p.xData = x;
851 p.yData = y;
852 p.xMin = Math.min(...p.xData);
853 p.xMax = Math.max(...p.xData);
854 p.yMin = Math.min(...p.yData);
855 p.yMax = Math.max(...p.yData);
856 }
857
858 if (!this.blockAutoScale) {
859 this.calcMinMax();
860 }
861
862 this.redraw();
863}
864
865MPlotGraph.prototype.resize = function () {
866 /**
867 * Resize canvas and redraw
868 */
869 this.canvas.width = this.parentDiv.clientWidth;
870 this.canvas.height = this.parentDiv.clientHeight;
871
872 this.redraw();
873}
874
875MPlotGraph.prototype.redraw = function () {
876 let f = this.draw.bind(this);
877 window.requestAnimationFrame(f);
878}
879
880MPlotGraph.prototype.xToScreen = function (x) {
881 /**
882 * Convert data coordinates to screen coordinates x axis
883 */
884
885 if (this.param.xAxis.log) {
886 if (x <= 0)
887 return this.x1;
888 else
889 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
890 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
891 }
892 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
893}
894
895MPlotGraph.prototype.yToScreen = function (y) {
896 /**
897 * Convert data coordinates to screen coordinates y axis
898 */
899 if (this.param.yAxis.log) {
900 if (y <= 0)
901 return this.y1;
902 else
903 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
904 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
905 }
906 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
907}
908
909MPlotGraph.prototype.screenToX = function (x) {
910 /**
911 * Convert screen coordinates to data coordinates x axis
912 */
913 if (this.param.xAxis.log) {
914 let xl = (x - this.x1) / (this.x2 - this.x1) * (Math.log(this.xMax)-Math.log(this.xMin)) + Math.log(this.xMin);
915 return Math.exp(xl);
916 }
917 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
918};
919
920MPlotGraph.prototype.screenToY = function (y) {
921 /**
922 * Convert screen coordinates to data coordinates y axis
923 */
924 if (this.param.yAxis.log) {
925 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
926 return Math.exp(yl);
927 }
928 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
929};
930
931MPlotGraph.prototype.calcMinMax = function () {
932
933 // simple nx / ny for colormaps
934 if (this.param.plot[0].type === "colormap") {
935 this.nx = this.param.plot[0].nx;
936 this.ny = this.param.plot[0].ny;
937
938 if (this.param.zAxis.min !== undefined)
939 this.zMin = this.param.zAxis.min;
940 else
941 this.zMin = this.param.plot[0].zMin;
942
943 if (this.param.zAxis.max !== undefined)
944 this.zMax = this.param.zAxis.max;
945 else
946 this.zMax = this.param.plot[0].zMax;
947
948 if (this.param.zAxis.log) {
949 if (this.zMin < 1E-20)
950 this.zMin = 1E-20;
951 if (this.zMax < 1E-18)
952 this.zMax = 1E-18;
953 }
954
955 this.xMin = this.param.plot[0].xMin;
956 this.xMax = this.param.plot[0].xMax;
957 this.yMin = this.param.plot[0].yMin;
958 this.yMax = this.param.plot[0].yMax;
959
960 this.xMin0 = this.xMin;
961 this.xMax0 = this.xMax;
962 this.yMin0 = this.yMin;
963 this.yMax0 = this.yMax;
964 return;
965 }
966
967 // determine min/max of overall plot
968 let xMin = this.param.plot[0].xMin;
969 for (const p of this.param.plot)
970 if (p.xMin < xMin)
971 xMin = p.xMin;
972 if (this.param.xAxis.min !== undefined)
973 xMin = this.param.xAxis.min;
974
975 let xMax = this.param.plot[0].xMax;
976 for (const p of this.param.plot)
977 if (p.xMax > xMax)
978 xMax = p.xMax;
979 if (this.param.xAxis.max !== undefined)
980 xMax = this.param.xAxis.max;
981
982 let yMin = this.param.plot[0].yMin;
983 for (const p of this.param.plot)
984 if (p.yMin < yMin)
985 yMin = p.yMin;
986 if (this.param.yAxis.min !== undefined)
987 yMin = this.param.yAxis.min;
988
989 let yMax = this.param.plot[0].yMax;
990 for (const p of this.param.plot)
991 if (p.yMax > yMax)
992 yMax = p.yMax;
993 if (this.param.yAxis.max !== undefined)
994 yMax = this.param.yAxis.max;
995
996 // avoid min === max
997 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
998 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
999
1000 // add 5% on each side
1001 let dx = (xMax - xMin);
1002 let dy = (yMax - yMin);
1003 if (this.param.plot[0].type !== "histogram" && this.param.plot[0].type !== "category") {
1004 if (this.param.xAxis.min === undefined)
1005 xMin -= dx / 20;
1006 if (this.param.xAxis.max === undefined)
1007 xMax += dx / 20;
1008 if (this.param.yAxis.min === undefined)
1009 yMin -= dy / 20;
1010 }
1011 if (this.param.yAxis.max === undefined)
1012 yMax += dy / 20;
1013
1014 this.xMin = xMin;
1015 this.xMax = xMax;
1016 this.yMin = yMin;
1017 this.yMax = yMax;
1018
1019 this.xMin0 = xMin;
1020 this.xMax0 = xMax;
1021 this.yMin0 = yMin;
1022 this.yMax0 = yMax;
1023}
1024
1025MPlotGraph.prototype.calcStats = function() {
1026 this.stats = {};
1027 let p = this.param.plot[0];
1028
1029 if (p.type === "scatter") {
1030 this.stats.name = ["Entries", "Mean X", "Std Dev X", "Mean Y", "Std Dev Y"];
1031
1032 this.stats.value = Array(5).fill(0);
1033 let n = this.param.plot[0].xData.length;
1034
1035 if (n > 1) {
1036 let mean = p.xData.reduce((sum, x) => sum + x, 0) / n;
1037 let variance = p.xData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1038 let stddev = Math.sqrt(variance);
1039 this.stats.value[0] = n;
1040 this.stats.value[1] = Number(mean.toPrecision(6));
1041 this.stats.value[2] = Number(stddev.toPrecision(6));
1042
1043 mean = p.yData.reduce((sum, x) => sum + x, 0) / n;
1044 variance = p.yData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1045 stddev = Math.sqrt(variance);
1046 this.stats.value[3] = Number(mean.toPrecision(6));
1047 this.stats.value[4] = Number(stddev.toPrecision(6));
1048 }
1049 }
1050
1051 if (p.type === "histogram") {
1052 this.stats.name = ["Entries", "Mean", "Std Dev"];
1053
1054 this.stats.value = Array(3).fill(0);
1055 let n = p.yData.reduce((sum, y) => sum + y, 0);
1056
1057 if (n > 1) {
1058 let sumY = 0;
1059 let sumXY = 0;
1060 let sumX2Y = 0;
1061
1062 for (let i=0 ; i< p.xData.length ; i++) {
1063 sumY += p.yData[i];
1064 sumXY += p.xData[i] * p.yData[i];
1065 sumX2Y += p.xData[i] * p.xData[i] * p.yData[i];
1066 }
1067
1068 let mean = sumXY / sumY;
1069 let variance = sumX2Y / sumY - mean ** 2;
1070 let stddev = Math.sqrt(variance);
1071 this.stats.value[0] = Number(n);
1072 this.stats.value[1] = Number(mean.toPrecision(6));
1073 this.stats.value[2] = Number(stddev.toPrecision(6));
1074 }
1075 }
1076
1077 if (p.type === "colormap") {
1078 this.stats.name = ["Entries", "Mean X", "Mean Y", "Std Dev X", "Std Dev Y"];
1079
1080 this.stats.value = Array(5).fill(0);
1081
1082 if (p.nx > 1 && p.ny > 1) {
1083 let n = 0;
1084
1085 // calculate x/y values
1086 let dx = (p.xMax - p.xMin) / this.nx;
1087 let dy = (p.yMax - p.yMin) / this.ny;
1088
1089 let xi = Array.from({ length: p.nx }, (_, i) =>
1090 p.xMin + (i + 0.5) * dx);
1091
1092 // sum up all columns projected to X-axis
1093 let sumH = Array(p.nx).fill(0);
1094 for (let i=0 ; i<p.nx ; i++) {
1095 for (let j = 0; j < p.ny; j++) {
1096 n += p.zData[i + j * p.nx];
1097 sumH[i] += p.zData[i + j * p.nx];
1098 }
1099 }
1100
1101 let sumY = 0;
1102 let sumXY = 0;
1103 let sumX2Y = 0;
1104
1105 for (let i=0 ; i< p.nx ; i++) {
1106 sumY += sumH[i];
1107 sumXY += xi[i] * sumH[i];
1108 sumX2Y += xi[i] * xi[i] * sumH[i];
1109 }
1110
1111 let mean = sumXY / sumY;
1112 let variance = sumX2Y / sumY - mean ** 2;
1113 let stddev = Math.sqrt(variance);
1114 this.stats.value[0] = Number(n.toPrecision(6));
1115 this.stats.value[1] = Number(mean.toPrecision(6));
1116 this.stats.value[3] = Number(stddev.toPrecision(6));
1117
1118 //----------------------------------------------
1119
1120 xi = Array.from({ length: p.ny }, (_, i) =>
1121 p.yMin + (i + 0.5) * dy);
1122
1123 // sup up all rows projected to Y-axis
1124 sumH = Array(p.ny).fill(0);
1125 for (let i=0 ; i<p.ny ; i++) {
1126 for (let j = 0; j < p.nx; j++) {
1127 sumH[i] += p.zData[j + i * p.nx];
1128 }
1129 }
1130
1131 sumY = 0;
1132 sumXY = 0;
1133 sumX2Y = 0;
1134
1135 for (let i=0 ; i< p.ny ; i++) {
1136 sumY += sumH[i];
1137 sumXY += xi[i] * sumH[i];
1138 sumX2Y += xi[i] * xi[i] * sumH[i];
1139 }
1140
1141 mean = sumXY / sumY;
1142 variance = sumX2Y / sumY - mean ** 2;
1143 stddev = Math.sqrt(variance);
1144 this.stats.value[2] = Number(mean.toPrecision(6));
1145 this.stats.value[4] = Number(stddev.toPrecision(6));
1146 }
1147 }
1148
1149}
1150
1151MPlotGraph.prototype.drawMarker = function(ctx, p, x, y) {
1152 /**
1153 * Draw a single marker on plot
1154 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1155 * @param {object} p param object from MPlotGraph
1156 * @param {number} x x coord of marker
1157 * @param {number} y y coord of marker
1158 */
1159 if (typeof p.marker.lineColor === "string")
1160 ctx.strokeStyle = p.marker.lineColor;
1161 else if (typeof p.marker.lineColor === "number")
1162 ctx.strokeStyle = this.param.color.data[p.marker.lineColor];
1163
1164 if (typeof p.marker.fillColor === "string")
1165 ctx.fillStyle = p.marker.fillColor;
1166 else if (typeof p.marker.fillColor === "number")
1167 ctx.fillStyle = this.param.color.data[p.marker.fillColor];
1168
1169 let size = p.marker.size;
1170 ctx.lineWidth = p.marker.lineWidth;
1171
1172 switch(p.marker.style) {
1173 case "circle":
1174 ctx.beginPath();
1175 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
1176 ctx.fill();
1177 ctx.stroke();
1178 break;
1179 case "square":
1180 ctx.fillRect(x - size / 2, y - size / 2, size, size);
1181 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
1182 break;
1183 case "diamond":
1184 ctx.beginPath();
1185 ctx.moveTo(x, y - size / 2);
1186 ctx.lineTo(x + size / 2, y);
1187 ctx.lineTo(x, y + size / 2);
1188 ctx.lineTo(x - size / 2, y);
1189 ctx.lineTo(x, y - size / 2);
1190 ctx.fill();
1191 ctx.stroke();
1192 break;
1193 case "pentagon":
1194 ctx.beginPath();
1195 ctx.moveTo(x + size * 0.00, y - size * 0.50);
1196 ctx.lineTo(x + size * 0.48, y - size * 0.16);
1197 ctx.lineTo(x + size * 0.30, y + size * 0.41);
1198 ctx.lineTo(x - size * 0.30, y + size * 0.41);
1199 ctx.lineTo(x - size * 0.48, y - size * 0.16);
1200 ctx.lineTo(x + size * 0.00, y - size * 0.50);
1201 ctx.fill();
1202 ctx.stroke();
1203 break;
1204 case "triangle-up":
1205 ctx.beginPath();
1206 ctx.moveTo(x, y - size / 2);
1207 ctx.lineTo(x + size / 2, y + size / 2);
1208 ctx.lineTo(x - size / 2, y + size / 2);
1209 ctx.lineTo(x, y - size / 2);
1210 ctx.fill();
1211 ctx.stroke();
1212 break;
1213 case "triangle-down":
1214 ctx.beginPath();
1215 ctx.moveTo(x, y + size / 2);
1216 ctx.lineTo(x + size / 2, y - size / 2);
1217 ctx.lineTo(x - size / 2, y - size / 2);
1218 ctx.lineTo(x, y + size / 2);
1219 ctx.fill();
1220 ctx.stroke();
1221 break;
1222 case "triangle-left":
1223 ctx.beginPath();
1224 ctx.moveTo(x - size / 2, y);
1225 ctx.lineTo(x + size / 2, y - size / 2);
1226 ctx.lineTo(x + size / 2, y + size / 2);
1227 ctx.lineTo(x - size / 2, y);
1228 ctx.fill();
1229 ctx.stroke();
1230 break;
1231 case "triangle-right":
1232 ctx.beginPath();
1233 ctx.moveTo(x + size / 2, y);
1234 ctx.lineTo(x - size / 2, y - size / 2);
1235 ctx.lineTo(x - size / 2, y + size / 2);
1236 ctx.lineTo(x + size / 2, y);
1237 ctx.fill();
1238 ctx.stroke();
1239 break;
1240 case "cross":
1241 ctx.beginPath();
1242 ctx.moveTo(x - size / 2, y - size / 2);
1243 ctx.lineTo(x + size / 2, y + size / 2);
1244 ctx.moveTo(x - size / 2, y + size / 2);
1245 ctx.lineTo(x + size / 2, y - size / 2);
1246 ctx.stroke();
1247 break;
1248 case "plus":
1249 ctx.beginPath();
1250 ctx.moveTo(x - size / 2, y);
1251 ctx.lineTo(x + size / 2, y);
1252 ctx.moveTo(x, y + size / 2);
1253 ctx.lineTo(x, y - size / 2);
1254 ctx.stroke();
1255 break;
1256 }
1257}
1258
1259MPlotGraph.prototype.drawXErrorBar = function(ctx, p, x, y, x1, x2) {
1260 /**
1261 * Draw a single horizontal errorbar on the plot
1262 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1263 * @param {object} p param object from MPlotGraph
1264 * @param {number} x x coord of bar center
1265 * @param {number} y y coord of bar center
1266 * @param {number} x1 low side error bar length
1267 * @param {number} x2 high side error bar length
1268 */
1269
1270 let size = p.marker.size / 2;
1271
1272 ctx.beginPath();
1273 ctx.moveTo(x1, y);
1274 ctx.lineTo(x2, y);
1275 ctx.moveTo(x1, y-size);
1276 ctx.lineTo(x1, y+size);
1277 ctx.moveTo(x2, y-size);
1278 ctx.lineTo(x2, y+size);
1279 ctx.stroke();
1280}
1281
1282MPlotGraph.prototype.drawYErrorBar = function(ctx, p, x, y, y1, y2) {
1283 /**
1284 * Draw a single vertical errorbar on the plot
1285 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1286 * @param {object} p param object from MPlotGraph
1287 * @param {number} x x coord of bar center
1288 * @param {number} y y coord of bar center
1289 * @param {number} y1 low side error bar length
1290 * @param {number} y2 high side error bar length
1291 */
1292
1293 let size = p.marker.size / 2;
1294
1295 ctx.beginPath();
1296 ctx.moveTo(x, y1);
1297 ctx.lineTo(x, y2);
1298 ctx.moveTo(x-size, y1);
1299 ctx.lineTo(x+size, y1);
1300 ctx.moveTo(x-size, y2);
1301 ctx.lineTo(x+size, y2);
1302 ctx.stroke();
1303}
1304
1305MPlotGraph.prototype.draw = function () {
1306 /**
1307 * draw the plot
1308 */
1309 //profile();
1310
1311 if (!this.canvas)
1312 return;
1313
1314 let ctx = this.canvas.getContext("2d");
1315
1316 this.width = this.canvas.width;
1317 this.height = this.canvas.height;
1318
1319 ctx.fillStyle = this.param.color.background;
1320 ctx.fillRect(0, 0, this.width, this.height);
1321
1322 if (this.error !== null) {
1323 ctx.lineWidth = 1;
1324 ctx.font = "14px sans-serif";
1325 ctx.strokeStyle = "#808080";
1326 ctx.fillStyle = "#808080";
1327 ctx.textAlign = "center";
1328 ctx.textBaseline = "middle";
1329 ctx.fillText(this.error, this.width / 2, this.height / 2);
1330 return;
1331 }
1332
1333 if (this.param.plot[0].xData === undefined) {
1334 ctx.lineWidth = 1;
1335 ctx.font = "14px sans-serif";
1336 ctx.strokeStyle = "#808080";
1337 ctx.fillStyle = "#808080";
1338 ctx.textAlign = "center";
1339 ctx.textBaseline = "middle";
1340 ctx.fillText("No data-odb-path present and no setData() called", this.width / 2, this.height / 2);
1341 return;
1342 }
1343
1344 if (this.height === undefined || this.width === undefined)
1345 return;
1346 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1347 return;
1348
1349 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1350
1351 let axisLabelWidth = this.drawYAxis(ctx, 50, this.height - 25, this.height - 35,
1352 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.param.yAxis.log, false);
1353
1354 if (axisLabelWidth === undefined)
1355 return;
1356
1357 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1358 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1359 else
1360 this.x1 = axisLabelWidth + 15;
1361
1362 this.x2 = this.param.showMenuButtons ? this.width - 30 : this.width - 2;
1363 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "")
1364 this.x2 -= 1.0*this.param.zAxis.title.textSize;
1365
1366 if (this.param.showMenuButtons === false)
1367 this.x2 = this.width - 2;
1368
1369 this.y1 = this.height;
1370 this.y2 = 6;
1371
1372 let axisLabelHeight;
1373 if (this.param.xAxis.type === "category")
1374 axisLabelHeight = this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1375 10, 12, this.param.plot[0].xData, false);
1376 else
1377 axisLabelHeight = this.param.xAxis.textSize;
1378
1379 axisLabelHeight += 12; // space for ticks and frame
1380
1381 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "")
1382 this.y1 = this.height - axisLabelHeight - 1.5*this.param.xAxis.title.textSize;
1383 else
1384 this.y1 = this.height - axisLabelHeight;
1385
1386 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1387 if (this.zMin === undefined || this.zMax === undefined) {
1388 this.zMin = 0;
1389 this.zMax = 1;
1390 }
1391 if (this.zMin === this.zMax) {
1392 this.zMin -= 0.5;
1393 this.zMax += 0.5;
1394 }
1395
1396 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1397 axisLabelWidth = this.drawYAxis(ctx, this.x2 + 30, this.y1, this.y1 - this.y2,
1398 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, false);
1399 if (axisLabelWidth === undefined)
1400 return;
1401
1402 if (this.param.zAxis.show) {
1403 let w = 5; // left gap
1404 w += 10; // color bar
1405 w += 12; // tick width
1406 w += 5;
1407
1408 this.x2 -= axisLabelWidth + w;
1409 this.param.zAxis.width = axisLabelWidth + w;
1410 }
1411 }
1412
1413 // title
1414 if (this.param.title.text !== "") {
1415 ctx.strokeStyle = this.param.color.axis;
1416 ctx.fillStyle = "#F0F0F0";
1417 ctx.font = this.param.title.textSize + "px sans-serif";
1418 let h = this.param.title.textSize * 1.2;
1419 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, h);
1420 ctx.fillRect(this.x1, 6, this.x2 - this.x1, h);
1421 ctx.textAlign = "center";
1422 ctx.textBaseline = "middle";
1423 ctx.fillStyle = this.param.title.color;
1424 ctx.fillText(this.param.title.text, (this.x2 + this.x1) / 2, 6 + h/2);
1425 this.y2 = 6 + h;
1426 }
1427
1428 // draw axis
1429 ctx.strokeStyle = this.param.color.axis;
1430
1431 if (this.param.yAxis.log && this.yMin < 1E-20)
1432 this.yMin = 1E-20;
1433 if (this.param.yAxis.log && this.yMax < 1E-18)
1434 this.yMax = 1E-18;
1435
1436 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
1437 ctx.save();
1438 ctx.fillStyle = this.param.title.color;
1439 let s = this.param.xAxis.title.textSize;
1440 ctx.font = s + "px sans-serif";
1441 ctx.textAlign = "center";
1442 ctx.textBaseline = "top";
1443 ctx.fillText(this.param.xAxis.title.text, (this.x1 + this.x2)/2,
1444 this.y1 + this.param.xAxis.textSize + 10 + this.param.xAxis.title.textSize / 4);
1445 ctx.restore();
1446 }
1447
1448 ctx.font = this.param.xAxis.textSize + "px sans-serif";
1449 let grid = this.param.xAxis.grid ? this.y2 - this.y1 : 0;
1450
1451 if (this.param.xAxis.type === "numeric")
1452 this.drawXAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1453 4, 7, 10, 10, grid, this.xMin, this.xMax, this.param.xAxis.log);
1454 else if (this.param.xAxis.type === "datetime")
1455 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
1456 4, 7, 10, 10, grid, this.xMin, this.xMax);
1457 else if (this.param.xAxis.type === "category")
1458 this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1459 10, 12, this.param.plot[0].xData, true);
1460
1461 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1462 ctx.save();
1463 ctx.fillStyle = this.param.title.color;
1464 let s = this.param.yAxis.title.textSize;
1465 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1466 ctx.rotate(-Math.PI / 2);
1467 ctx.font = s + "px sans-serif";
1468 ctx.textAlign = "center";
1469 ctx.textBaseline = "top";
1470 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1471 ctx.restore();
1472 }
1473
1474 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
1475 ctx.save();
1476 ctx.fillStyle = this.param.title.color;
1477 let s = this.param.zAxis.title.textSize;
1478 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1479 ctx.rotate(-Math.PI / 2);
1480 ctx.font = s + "px sans-serif";
1481 ctx.textAlign = "center";
1482 ctx.textBaseline = "middle";
1483 ctx.fillText(this.param.zAxis.title.text, 0, this.x2 + this.param.zAxis.width);
1484 ctx.restore();
1485 }
1486
1487 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1488 grid = this.param.yAxis.grid ? this.x2 - this.x1 : 0;
1489 this.drawYAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
1490 -4, -7, -10, -12, grid, this.yMin, this.yMax, this.param.yAxis.log, true);
1491
1492 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1493 ctx.save();
1494 let s = this.param.yAxis.title.textSize;
1495 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1496 ctx.rotate(-Math.PI / 2);
1497 ctx.font = s + "px sans-serif";
1498 ctx.textAlign = "center";
1499 ctx.textBaseline = "top";
1500 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1501 ctx.restore();
1502 }
1503
1504 // draw frame
1505 ctx.strokeStyle = this.param.color.axis;
1506 ctx.strokeRect(this.x1, this.y1, this.x2-this.x1, this.y2-this.y1);
1507
1508 // set clipping region not to draw outside axes
1509 ctx.save();
1510 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
1511 ctx.clip();
1512
1513 // draw graphs
1514 let noData = true;
1515 for (const p of this.param.plot) {
1516 if (p.xData === undefined || p.xData === null)
1517 continue;
1518
1519 if (p.xData.length > 0)
1520 noData = false;
1521
1522 ctx.globalAlpha = p.alpha;
1523
1524 if (p.type === "scatter") {
1525 // draw lines
1526 if (p.line && p.line.draw ||
1527 p.line && p.line.fill) {
1528
1529 if (typeof p.line.color === "string")
1530 ctx.fillStyle = p.line.color;
1531 else if (typeof p.line.color === "number")
1532 ctx.fillStyle = this.param.color.data[p.line.color];
1533 ctx.strokeStyle = ctx.fillStyle;
1534
1535 // shaded area
1536 if (p.line.fill) {
1537 ctx.globalAlpha = 0.1;
1538 ctx.beginPath();
1539 ctx.moveTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1540 for (let i = 0; i < p.xData.length; i++) {
1541 let x = this.xToScreen(p.xData[i]);
1542 let y = this.yToScreen(p.yData[i]);
1543 ctx.lineTo(x, y);
1544 }
1545 ctx.lineTo(this.xToScreen(p.xData[p.xData.length - 1]), this.yToScreen(0));
1546 ctx.lineTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1547 ctx.fill();
1548 ctx.globalAlpha = 1;
1549 }
1550
1551 // draw line
1552 if (p.line.draw && p.line.width > 0) {
1553 ctx.lineWidth = p.line.width;
1554 ctx.beginPath();
1555 for (let i = 0; i < p.xData.length; i++) {
1556 let x = this.xToScreen(p.xData[i]);
1557 let y = this.yToScreen(p.yData[i]);
1558 if (i === 0)
1559 ctx.moveTo(x, y);
1560 else
1561 ctx.lineTo(x, y);
1562 }
1563 ctx.stroke();
1564 }
1565 }
1566
1567 // draw markers
1568 if (p.marker && p.marker.draw) {
1569 for (let i = 0; i < p.xData.length; i++) {
1570
1571 let x = this.xToScreen(p.xData[i]);
1572 let y = this.yToScreen(p.yData[i]);
1573
1574 this.drawMarker(ctx, p, x, y);
1575
1576 if (p.xErrorData) {
1577 let x1 = this.xToScreen(p.xData[i]-p.xErrorData[i]);
1578 let x2 = this.xToScreen(p.xData[i]+p.xErrorData[i]);
1579 this.drawXErrorBar(ctx, p, x, y, x1, x2);
1580 }
1581
1582 if (p.yErrorData) {
1583 let y1 = this.yToScreen(p.yData[i]+p.yErrorData[i]);
1584 let y2 = this.yToScreen(p.yData[i]-p.yErrorData[i]);
1585 this.drawYErrorBar(ctx, p, x, y, y1, y2);
1586 }
1587 }
1588 }
1589 }
1590
1591 else if (p.type === "histogram") {
1592 let x, y;
1593 let dx = (p.xMax - p.xMin) / p.xData.length;
1594 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1595
1596 if (p.length < 100)
1597 ctx.lineWidth = 2;
1598 else
1599 ctx.lineWidth = 1;
1600
1601 if (typeof p.line.color === "string")
1602 ctx.fillStyle = p.line.color;
1603 else if (typeof p.line.color === "number")
1604 ctx.fillStyle = this.param.color.data[p.line.color];
1605 ctx.strokeStyle = ctx.fillStyle;
1606
1607 ctx.beginPath();
1608 ctx.moveTo(this.xToScreen(p.xData[0])-dxs/2, this.yToScreen(0));
1609 for (let i = 0; i < p.xData.length; i++) {
1610 x = this.xToScreen(p.xData[i]);
1611 y = this.yToScreen(p.yData[i]);
1612 ctx.lineTo(x-dxs/2, y);
1613 ctx.lineTo(x+dxs/2, y);
1614 }
1615 ctx.lineTo(x+dxs/2, this.yToScreen(0));
1616 ctx.globalAlpha = 0.2;
1617 ctx.fill();
1618 ctx.globalAlpha = 1;
1619 ctx.stroke();
1620 }
1621
1622 else if (p.type === "category") {
1623 let x, y;
1624 let dx = (this.x2 - this. x1) / p.xData.length;
1625
1626 let width;
1627 if (p.barWidth)
1628 width = dx * p.barWidth;
1629 else
1630 width = dx * 0.3;
1631
1632 if (p.xData.length < 100)
1633 ctx.lineWidth = 2;
1634 else
1635 ctx.lineWidth = 1;
1636
1637 if (typeof p.line.color === "string")
1638 ctx.fillStyle = p.line.color;
1639 else if (typeof p.line.color === "number")
1640 ctx.fillStyle = this.param.color.data[p.line.color];
1641 ctx.strokeStyle = ctx.fillStyle;
1642
1643 ctx.beginPath();
1644 for (let i = 0; i < p.xData.length; i++) {
1645 x = this.xToScreen(i + 0.5);
1646 y = this.yToScreen(p.yData[i]);
1647 ctx.moveTo(x-width/2, this.yToScreen(0));
1648 ctx.lineTo(x-width/2, y);
1649 ctx.lineTo(x+width/2, y);
1650 ctx.lineTo(x+width/2, this.yToScreen(0));
1651 ctx.lineTo(x-width/2, this.yToScreen(0));
1652 }
1653 ctx.globalAlpha = 0.2;
1654 ctx.fill();
1655 ctx.globalAlpha = 1;
1656 ctx.stroke();
1657 }
1658
1659 else if (p.type === "colormap") {
1660 let dx = (p.xMax - p.xMin) / this.nx;
1661 let dy = (p.yMax - p.yMin) / this.ny;
1662
1663 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1664 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
1665
1666 for (let i=0 ; i<p.ny ; i++) {
1667 for (let j=0 ; j<p.nx ; j++) {
1668 let x = this.xToScreen(j * dx + p.xMin);
1669 let y = this.yToScreen(i * dy + p.yMin);
1670 let zval = this.param.plot[0].zData[j+i*p.nx];
1671 if (isNaN(zval)) {
1672 ctx.fillStyle = 'hsl(255, 0%, 50%)';
1673 } else {
1674 let v;
1675 if (this.param.zAxis.log) {
1676 if (zval <= 0)
1677 v = 0;
1678 else
1679 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
1680 } else
1681 v = (zval - this.zMin) / (this.zMax - this.zMin);
1682
1683 // limit v to 0...1
1684 if (v < 0)
1685 v = 0;
1686 if (v > 1)
1687 v = 1;
1688
1689 if (zval < 0.5 && this.param.plot[0].bgcolor)
1690 ctx.fillStyle = this.param.plot[0].bgcolor;
1691 else
1692 ctx.fillStyle = 'hsl(' + Math.floor((1 - v) * 240) + ', 100%, 50%)';
1693 }
1694 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
1695 }
1696 }
1697 //profile("plot");
1698 }
1699 }
1700
1701 ctx.restore(); // remove clipping
1702
1703 // plot color scale
1704 if (this.param.plot[0].type === "colormap") {
1705 if (this.param.plot[0].showZScale) {
1706
1707 for (let i=0 ; i<100 ; i++) {
1708 let v = i / 100;
1709 ctx.fillStyle = 'hsl(' +
1710 Math.floor(v * 240) + ', 100%, 50%)';
1711 ctx.fillRect(this.x2 + 5, this.y2 + i/100*(this.y1 - this.y2),
1712 10, (this.y1 - this.y2) / 100 + 1);
1713 }
1714
1715 ctx.lineWidth = 1;
1716 ctx.strokeStyle = this.param.color.axis;
1717 ctx.beginPath();
1718 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
1719 ctx.stroke();
1720
1721 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1722 ctx.strokeStyle = this.param.color.axis;
1723
1724 this.drawYAxis(ctx, this.x2 + 15, this.y1, this.y1 - this.y2,
1725 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, true);
1726 }
1727 }
1728
1729 // plot legend
1730 let nLabel = 0;
1731 for (const p of this.param.plot)
1732 if (p.label && p.label !== "")
1733 nLabel++;
1734
1735 if (this.param.legend?.show && nLabel > 0) {
1736 ctx.font = this.param.legend.textSize + "px sans-serif";
1737
1738 let mw = 0;
1739 for (const p of this.param.plot) {
1740 if (ctx.measureText(p.label).width > mw) {
1741 mw = ctx.measureText(p.label).width;
1742 }
1743 }
1744 let w = 50 + mw + 5;
1745 let h = this.param.legend.textSize * 1.5;
1746
1747 ctx.fillStyle = this.param.legend.backgroundColor;
1748 ctx.strokeStyle = this.param.legend.color;
1749 ctx.fillRect(this.x1, this.y2, w, h * this.param.plot.length);
1750 ctx.strokeRect(this.x1, this.y2, w, h * this.param.plot.length);
1751
1752 for (const [pi,p] of this.param.plot.entries()) {
1753 if (p.line && p.line.draw && p.line.width > 0) {
1754 ctx.beginPath();
1755 ctx.strokeStyle = this.param.color.data[p.line.color];
1756 ctx.lineWidth = p.line.width;
1757 ctx.beginPath();
1758 ctx.moveTo(this.x1 + 5, this.y2 + pi*h + h/2);
1759 ctx.lineTo(this.x1 + 35, this.y2 + pi*h + h/2);
1760 ctx.stroke();
1761 }
1762 if (p.marker) {
1763 this.drawMarker(ctx, p, this.x1 + 20, this.y2 + pi*h + h/2);
1764 }
1765 ctx.textAlign = "left";
1766 ctx.textBaseline = "middle";
1767 ctx.fillStyle = this.param.color.axis;
1768 ctx.fillText(p.label, this.x1 + 40, this.y2 + pi*h + h/2);
1769 }
1770 }
1771
1772 this.calcStats();
1773
1774 // plot statistics
1775 if (this.param.stats.show && this.stats.name) {
1776 ctx.font = this.param.legend.textSize + "px sans-serif";
1777
1778 let mw = 0;
1779 for (const [si,s] of this.stats.name.entries()) {
1780 let str = s + " " + this.stats.value[si].toString();
1781 if (ctx.measureText(str).width > mw) {
1782 mw = ctx.measureText(str).width;
1783 }
1784 }
1785 let w = mw + 10;
1786 let h = this.param.legend.textSize * 1.5;
1787
1788 ctx.fillStyle = this.param.legend.backgroundColor;
1789 ctx.strokeStyle = this.param.legend.color;
1790 ctx.fillRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
1791 ctx.strokeRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
1792
1793 for (const [si,s] of this.stats.name.entries()) {
1794 ctx.textAlign = "left";
1795 ctx.textBaseline = "middle";
1796 ctx.fillStyle = this.param.color.axis;
1797 ctx.fillText(s, this.x2 - w + 5, this.y2 + si*h + h/2);
1798 ctx.textAlign = "right";
1799 let str = this.stats.value[si].toString();
1800 ctx.fillText(str, this.x2 - 5, this.y2 + si*h + h/2);
1801 }
1802 }
1803
1804 // "empty window" notice
1805 if (noData) {
1806 ctx.font = "16px sans-serif";
1807 let str = "No data available";
1808 ctx.strokeStyle = "#404040";
1809 ctx.fillStyle = "#F0F0F0";
1810 let w = ctx.measureText(str).width + 10;
1811 let h = 16 + 10;
1812 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1813 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1814 ctx.fillStyle = "#404040";
1815 ctx.textAlign = "center";
1816 ctx.textBaseline = "middle";
1817 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
1818 ctx.font = "14px sans-serif";
1819 }
1820
1821 // buttons
1822 if (this.param.showMenuButtons) {
1823 let y = 0;
1824 let buttonSize = 20;
1825 this.button.forEach(b => {
1826
1827 if (!(this.param.plot[0].type === "category" && b.src === "stats.svg")) {
1828 b.x1 = this.width - buttonSize - 6;
1829 b.y1 = 6 + y * (buttonSize + 4);
1830 b.width = buttonSize + 4;
1831 b.height = buttonSize + 4;
1832 b.enabled = true;
1833
1834 ctx.fillStyle = "#F0F0F0";
1835 ctx.strokeStyle = "#808080";
1836 ctx.fillRect(b.x1, b.y1, b.width, b.height);
1837 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
1838 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
1839
1840 y++;
1841 }
1842 });
1843 }
1844
1845 // axis zoom
1846 if (this.zoom.x.active) {
1847 ctx.fillStyle = "#808080";
1848 ctx.globalAlpha = 0.2;
1849 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
1850 ctx.globalAlpha = 1;
1851 ctx.strokeStyle = "#808080";
1852 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
1853 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
1854 }
1855 if (this.zoom.y.active) {
1856 ctx.fillStyle = "#808080";
1857 ctx.globalAlpha = 0.2;
1858 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
1859 ctx.globalAlpha = 1;
1860 ctx.strokeStyle = "#808080";
1861 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
1862 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
1863 }
1864
1865 // marker
1866 if (this.marker.active) {
1867
1868 // round marker
1869 if (this.param.plot[0].type !== "colormap") {
1870 ctx.beginPath();
1871 ctx.globalAlpha = 0.1;
1872 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
1873 ctx.fillStyle = "#000000";
1874 ctx.fill();
1875 ctx.globalAlpha = 1;
1876
1877 ctx.beginPath();
1878 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
1879 ctx.fillStyle = "#000000";
1880 ctx.fill();
1881 }
1882
1883 ctx.strokeStyle = "#A0A0A0";
1884 ctx.drawLine(this.marker.sx, this.y1, this.marker.sx, this.y2);
1885 ctx.drawLine(this.x1, this.marker.sy, this.x2, this.marker.sy);
1886
1887 // text label
1888 ctx.font = "12px sans-serif";
1889 ctx.textAlign = "left";
1890 let s;
1891 if (this.parentDiv.dataset.tooltip) {
1892 let f = this.parentDiv.dataset.tooltip;
1893 if (f.indexOf('(') !== -1) // strip any '('
1894 f = f.substring(0, f.indexOf('('));
1895
1896 s = eval(f + "(this)");
1897 } else {
1898 s = this.marker.x.toPrecision(6).stripZeros() + " / " +
1899 this.marker.y.toPrecision(6).stripZeros();
1900 if (this.param.plot[0].type === "colormap")
1901 s += ": " + (this.marker.z === null ? "null" : this.marker.z.toPrecision(6).stripZeros());
1902 }
1903 let w = ctx.measureText(s).width + 6;
1904 let h = ctx.measureText("M").width * 1.2 + 6;
1905 let x = this.marker.mx + 10;
1906 let y = this.marker.my - 20;
1907
1908 // move marker inside if outside plotting area
1909 if (x + w >= this.x2)
1910 x = this.marker.sx - 10 - w;
1911
1912 ctx.strokeStyle = "#808080";
1913 ctx.fillStyle = "#F0F0F0";
1914 ctx.textBaseline = "middle";
1915 ctx.fillRect(x, y, w, h);
1916 ctx.strokeRect(x, y, w, h);
1917 ctx.fillStyle = "#404040";
1918 ctx.fillText(s, x + 3, y + h / 2);
1919 }
1920
1921 // call optional user overlay function
1922 if (this.param.overlay) {
1923
1924 // set default text
1925 ctx.textAlign = "left";
1926 ctx.textBaseline = "top";
1927 ctx.fillStyle = "black";
1928 ctx.strokeStyle = "black";
1929 ctx.font = "12px sans-serif";
1930
1931 eval(this.param.overlay + "(this, ctx)");
1932 }
1933
1934 //profile("end");
1935}
1936
1937LN10 = 2.302585094;
1938LOG2 = 0.301029996;
1939LOG5 = 0.698970005;
1940
1941MPlotGraph.prototype.drawXAxis = function (ctx, x1, y1, width, minor, major,
1942 text, label, grid, xmin, xmax, logaxis) {
1943 /** Draw the xaxis
1944 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1945 * @param {number} x1 coordinate position of the axis on screen
1946 * @param {number} y1 coordinate position of the axis on screen
1947 * @param {number} width width of the axis, likely also the width of the plot
1948 * @param {number} minor step between minor ticks
1949 * @param {number} major step between major ticks
1950 * @param {string} text
1951 * @param {string} label
1952 * @param {bool} grid if true draw grid lines over minor ticks
1953 * @param {number} ymin low limit of the axis
1954 * @param {number} ymax high limit of the axis
1955 * @param {bool} logaxis if true draw axis on a log scale (base 10)
1956 */
1957 var dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1958 var tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1959 var base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1960
1961 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
1962 return;
1963
1964 if (xmax <= xmin || width <= 0)
1965 return;
1966
1967 ctx.textAlign = "center";
1968 ctx.textBaseline = "top";
1969
1970 if (logaxis) {
1971
1972 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1973 if (isNaN(dx) || dx === 0) {
1974 xmin = 1E-20;
1975 dx = 1E-20;
1976 }
1977 label_dx = dx;
1978 major_dx = dx * 10;
1979 n_sig1 = 4;
1980
1981 } else { // linear axis ----
1982
1983 // use 10 as min tick distance
1984 dx = (xmax - xmin) / (width / 10);
1985
1986 int_dx = Math.floor(Math.log(dx) / LN10);
1987 frac_dx = Math.log(dx) / LN10 - int_dx;
1988
1989 if (frac_dx < 0) {
1990 frac_dx += 1;
1991 int_dx -= 1;
1992 }
1993
1994 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
1995 major_base = label_base = tick_base + 1;
1996
1997 // rounding up of dx, label_dx
1998 dx = Math.pow(10, int_dx) * base[tick_base];
1999 major_dx = Math.pow(10, int_dx) * base[major_base];
2000 label_dx = major_dx;
2001
2002 do {
2003 // number of significant digits
2004 if (xmin === 0)
2005 n_sig1 = 0;
2006 else
2007 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2008
2009 if (xmax === 0)
2010 n_sig2 = 0;
2011 else
2012 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2013
2014 n_sig1 = Math.max(n_sig1, n_sig2);
2015
2016 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2017 if (Math.abs(xmin) < 100000)
2018 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / LN10) + 1);
2019 if (Math.abs(xmax) < 100000)
2020 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / LN10) + 1);
2021
2022 // determination of maximal width of labels
2023 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
2024 let ext = ctx.measureText(str);
2025 maxwidth = ext.width;
2026
2027 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
2028 ext = ctx.measureText(str);
2029 maxwidth = Math.max(maxwidth, ext.width);
2030 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
2031 maxwidth = Math.max(maxwidth, ext.width);
2032
2033 // increasing label_dx, if labels would overlap
2034 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
2035 label_base++;
2036 label_dx = Math.pow(10, int_dx) * base[label_base];
2037 if (label_base % 3 === 2 && major_base % 3 === 1) {
2038 major_base++;
2039 major_dx = Math.pow(10, int_dx) * base[major_base];
2040 }
2041 } else
2042 break;
2043
2044 } while (true);
2045 }
2046
2047 x_act = Math.floor(xmin / dx) * dx;
2048
2049 ctx.strokeStyle = this.param.color.axis;
2050 ctx.drawLine(x1, y1, x1 + width, y1);
2051
2052 do {
2053 if (logaxis)
2054 x_screen = (Math.log(x_act) - Math.log(xmin)) /
2055 (Math.log(xmax) - Math.log(xmin)) * width + x1;
2056 else
2057 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
2058 xs = Math.floor(x_screen + 0.5);
2059
2060 if (x_screen > x1 + width + 0.001)
2061 break;
2062
2063 if (x_screen >= x1) {
2064 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
2065 dx / major_dx / 10.0) {
2066
2067 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
2068 dx / label_dx / 10.0) {
2069 // label tick mark
2070 ctx.strokeStyle = this.param.color.axis;
2071 ctx.drawLine(xs, y1, xs, y1 + text);
2072
2073 // grid line
2074 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2075 ctx.strokeStyle = this.param.color.grid;
2076 ctx.drawLine(xs, y1, xs, y1 + grid);
2077 }
2078
2079 // label
2080 if (label !== 0) {
2081 str = x_act.toPrecision(n_sig1).stripZeros();
2082 ext = ctx.measureText(str);
2083 if (xs - ext.width / 2 > x1 &&
2084 xs + ext.width / 2 < x1 + width) {
2085 ctx.strokeStyle = this.param.color.label;
2086 ctx.fillStyle = this.param.color.label;
2087 ctx.fillText(str, xs, y1 + label);
2088 }
2089 last_label_x = xs + ext.width / 2;
2090 }
2091 } else {
2092 // major tick mark
2093 ctx.strokeStyle = this.param.color.axis;
2094 ctx.drawLine(xs, y1, xs, y1 + major);
2095
2096 // grid line
2097 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2098 ctx.strokeStyle = this.param.color.grid;
2099 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2100 }
2101 }
2102
2103 if (logaxis) {
2104 dx *= 10;
2105 major_dx *= 10;
2106 label_dx *= 10;
2107 }
2108
2109 } else {
2110 // minor tick mark
2111 ctx.strokeStyle = this.param.color.axis;
2112 ctx.drawLine(xs, y1, xs, y1 + minor);
2113 }
2114
2115 if (logaxis) {
2116 // for log axis, also put grid lines on minor tick marks
2117 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2118 ctx.strokeStyle = this.param.color.grid;
2119 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2120 }
2121
2122 // for log axis, also put labels on minor tick marks
2123 if (label !== 0) {
2124 let str;
2125 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
2126 str = x_act.toExponential(n_sig1).stripZeros();
2127 else
2128 str = x_act.toPrecision(n_sig1).stripZeros();
2129 ext = ctx.measureText(str);
2130 if (xs - ext.width / 2 > x1 &&
2131 xs + ext.width / 2 < x1 + width &&
2132 xs - ext.width / 2 > last_label_x + 5) {
2133 ctx.strokeStyle = this.param.color.label;
2134 ctx.fillStyle = this.param.color.label;
2135 ctx.fillText(str, xs, y1 + label);
2136 }
2137
2138 last_label_x = xs + ext.width / 2;
2139 }
2140 }
2141 }
2142
2143 x_act += dx;
2144
2145 /* suppress 1.23E-17 ... */
2146 if (Math.abs(x_act) < dx / 100)
2147 x_act = 0;
2148
2149 } while (1);
2150}
2151
2152MPlotGraph.prototype.drawYAxis = function (ctx, x1, y1, height, minor, major,
2153 text, label, grid, ymin, ymax, logaxis, draw) {
2154 /** Draw the yaxis
2155 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2156 * @param {number} x1 coordinate position of the axis on screen
2157 * @param {number} y1 coordinate position of the axis on screen
2158 * @param {number} height height of the axis, likely also the height of the plot
2159 * @param {number} minor step between minor ticks
2160 * @param {number} major step between major ticks
2161 * @param {string} text
2162 * @param {string} label
2163 * @param {bool} grid if true draw grid lines over minor ticks
2164 * @param {number} ymin low limit of the axis
2165 * @param {number} ymax high limit of the axis
2166 * @param {bool} logaxis if true draw axis on a log scale (base 10)
2167 * @param {bool} draw if true draw the axis
2168 */
2169 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
2170 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
2171 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
2172
2173 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
2174 return;
2175
2176 if (ymax <= ymin || height <= 0)
2177 return;
2178
2179 if (label < 0)
2180 ctx.textAlign = "right";
2181 else
2182 ctx.textAlign = "left";
2183 ctx.textBaseline = "middle";
2184 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
2185
2186 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
2187 dy = Number.MAX_VALUE / 10;
2188 label_dy = dy;
2189 major_dy = dy;
2190 n_sig1 = 1;
2191 } else if (logaxis) {
2192 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
2193 if (isNaN(dy) || dy === 0) {
2194 ymin = 1E-20;
2195 dy = 1E-20;
2196 }
2197 label_dy = dy;
2198 major_dy = dy * 10;
2199 n_sig1 = 4;
2200 } else {
2201 // use 6 as min tick distance
2202 dy = (ymax - ymin) / (height / 6);
2203
2204 int_dy = Math.floor(Math.log(dy) / Math.log(10));
2205 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
2206
2207 if (frac_dy < 0) {
2208 frac_dy += 1;
2209 int_dy -= 1;
2210 }
2211
2212 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
2213 major_base = label_base = tick_base + 1;
2214
2215 // rounding up of dy, label_dy
2216 dy = Math.pow(10, int_dy) * base[tick_base];
2217 major_dy = Math.pow(10, int_dy) * base[major_base];
2218 label_dy = major_dy;
2219
2220 // number of significant digits
2221 if (ymin === 0)
2222 n_sig1 = 1;
2223 else
2224 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
2225 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2226
2227 if (ymax === 0)
2228 n_sig2 = 1;
2229 else
2230 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
2231 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2232
2233 n_sig1 = Math.max(n_sig1, n_sig2);
2234 n_sig1 = Math.max(1, n_sig1);
2235
2236 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2237 if (Math.abs(ymin) < 100000)
2238 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
2239 Math.log(10) + 0.001) + 1);
2240 if (Math.abs(ymax) < 100000)
2241 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
2242 Math.log(10) + 0.001) + 1);
2243
2244 // increase label_dy if labels would overlap
2245 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
2246 label_base++;
2247 label_dy = Math.pow(10, int_dy) * base[label_base];
2248 if (label_base % 3 === 2 && major_base % 3 === 1) {
2249 major_base++;
2250 major_dy = Math.pow(10, int_dy) * base[major_base];
2251 }
2252 }
2253 }
2254
2255 y_act = Math.floor(ymin / dy) * dy;
2256
2257 let last_label_y = y1;
2258 let maxwidth = 0;
2259
2260 if (draw) {
2261 ctx.strokeStyle = this.param.color.axis;
2262 ctx.drawLine(x1, y1, x1, y1 - height);
2263 }
2264
2265 do {
2266 if (logaxis)
2267 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
2268 (Math.log(ymax) - Math.log(ymin)) * height;
2269 else if (!(isFinite(ymax - ymin)))
2270 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
2271 else
2272 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
2273 ys = Math.round(y_screen);
2274
2275 if (y_screen < y1 - height - 0.001 || isNaN(ys))
2276 break;
2277
2278 if (y_screen <= y1 + 0.001) {
2279 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
2280 dy / major_dy / 10.0) {
2281
2282 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
2283 dy / label_dy / 10.0) {
2284 // label tick mark
2285 if (draw) {
2286 ctx.strokeStyle = this.param.color.axis;
2287 ctx.drawLine(x1, ys, x1 + text, ys);
2288 }
2289
2290 // grid line
2291 if (grid !== 0 && ys < y1 && ys > y1 - height)
2292 if (draw) {
2293 ctx.strokeStyle = this.param.color.grid;
2294 ctx.drawLine(x1, ys, x1 + grid, ys);
2295 }
2296
2297 // label
2298 if (label !== 0) {
2299 let str;
2300 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2301 str = y_act.toExponential(n_sig1).stripZeros();
2302 else
2303 str = y_act.toPrecision(n_sig1).stripZeros();
2304 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2305 if (draw) {
2306 ctx.strokeStyle = this.param.color.label;
2307 ctx.fillStyle = this.param.color.label;
2308 ctx.fillText(str, x1 + label, ys);
2309 }
2310 last_label_y = ys - textHeight / 2;
2311 }
2312 } else {
2313 // major tick mark
2314 if (draw) {
2315 ctx.strokeStyle = this.param.color.axis;
2316 ctx.drawLine(x1, ys, x1 + major, ys);
2317 }
2318
2319 // grid line
2320 if (grid !== 0 && ys < y1 && ys > y1 - height)
2321 if (draw) {
2322 ctx.strokeStyle = this.param.color.grid;
2323 ctx.drawLine(x1, ys, x1 + grid, ys);
2324 }
2325 }
2326
2327 if (logaxis) {
2328 dy *= 10;
2329 major_dy *= 10;
2330 label_dy *= 10;
2331 }
2332
2333 } else {
2334 // minor tick mark
2335 if (draw) {
2336 ctx.strokeStyle = this.param.color.axis;
2337 ctx.drawLine(x1, ys, x1 + minor, ys);
2338 }
2339 }
2340
2341 if (logaxis) {
2342
2343 // for log axis, also put grid lines on minor tick marks
2344 if (grid !== 0 && ys < y1 && ys > y1 - height) {
2345 if (draw) {
2346 ctx.strokeStyle = this.param.color.grid;
2347 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
2348 }
2349 }
2350
2351 // for log axis, also put labels on minor tick marks
2352 if (label !== 0) {
2353 let str;
2354 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2355 str = y_act.toExponential(n_sig1).stripZeros();
2356 else
2357 str = y_act.toPrecision(n_sig1).stripZeros();
2358 if (ys - textHeight / 2 > y1 - height &&
2359 ys + textHeight / 2 < y1 &&
2360 ys + textHeight < last_label_y + 2) {
2361 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2362 if (draw) {
2363 ctx.strokeStyle = this.param.color.label;
2364 ctx.fillStyle = this.param.color.label;
2365 ctx.fillText(str, x1 + label, ys);
2366 }
2367 }
2368
2369 last_label_y = ys;
2370 }
2371 }
2372 }
2373
2374 y_act += dy;
2375
2376 // suppress 1.23E-17 ...
2377 if (Math.abs(y_act) < dy / 100)
2378 y_act = 0;
2379
2380 } while (1);
2381
2382 return maxwidth;
2383};
2384
2385/* Begin timeToLabel format options */
2386let options1 = {
2387 timeZone: 'UTC',
2388 day: '2-digit', month: 'short', year: '2-digit',
2389 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
2390};
2391
2392let options2 = {
2393 timeZone: 'UTC',
2394 day: '2-digit', month: 'short', year: '2-digit',
2395 hour12: false, hour: '2-digit', minute: '2-digit'
2396};
2397
2398let options3 = {
2399 timeZone: 'UTC',
2400 day: '2-digit', month: 'short', year: '2-digit',
2401 hour12: false, hour: '2-digit', minute: '2-digit'
2402};
2403
2404let options4 = {
2405 timeZone: 'UTC',
2406 day: '2-digit', month: 'short', year: '2-digit'
2407};
2408
2409let options5 = {
2410 timeZone: 'UTC',
2411 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
2412};
2413
2414let options6 = {
2415 timeZone: 'UTC',
2416 hour12: false, hour: '2-digit', minute: '2-digit'
2417};
2418
2419let options7 = {
2420 timeZone: 'UTC',
2421 hour12: false, hour: '2-digit', minute: '2-digit'
2422};
2423
2424let options8 = {
2425 timeZone: 'UTC',
2426 day: '2-digit', month: 'short', year: '2-digit',
2427 hour12: false, hour: '2-digit', minute: '2-digit'
2428};
2429
2430let options9 = {
2431 timeZone: 'UTC',
2432 day: '2-digit', month: 'short', year: '2-digit'
2433};
2434/* End timeToLabel format options */
2435
2436function timeToLabel(sec, base, forceDate) {
2437 /**
2438 * Convert time in seconds to a human-readable string
2439 * @param {number} sec number of seconds
2440 * @param {number} base chooses which display option to use on conversion
2441 * @param {bool} forceDate if true force showing the date, else can show only time
2442 * @returns {string} human-readable datetime as a string
2443 */
2444 let d = mhttpd_get_display_time(sec).date;
2445
2446 if (forceDate) {
2447 if (base < 60) {
2448 return d.toLocaleTimeString('en-GB', options1);
2449 } else if (base < 600) {
2450 return d.toLocaleTimeString('en-GB', options2);
2451 } else if (base < 3600 * 24) {
2452 return d.toLocaleTimeString('en-GB', options3);
2453 } else {
2454 return d.toLocaleDateString('en-GB', options4);
2455 }
2456 }
2457
2458 if (base < 60) {
2459 return d.toLocaleTimeString('en-GB', options5);
2460 } else if (base < 600) {
2461 return d.toLocaleTimeString('en-GB', options6);
2462 } else if (base < 3600 * 3) {
2463 return d.toLocaleTimeString('en-GB', options7);
2464 } else if (base < 3600 * 24) {
2465 return d.toLocaleTimeString('en-GB', options8);
2466 } else {
2467 return d.toLocaleDateString('en-GB', options9);
2468 }
2469}
2470
2471MPlotGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
2472 text, label, grid, xmin, xmax) {
2473 /** Draw the xaxis as time
2474 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2475 * @param {number} x1 coordinate position of the axis on screen
2476 * @param {number} y1 coordinate position of the axis on screen
2477 * @param {number} width width of the axis, likely also the width of the plot
2478 * @param {number} xr
2479 * @param {number} minor step between minor ticks
2480 * @param {number} major step between major ticks
2481 * @param {string} text
2482 * @param {string} label
2483 * @param {bool} grid if true draw grid lines over minor ticks
2484 * @param {number} xmin low limit of the axis
2485 * @param {number} xmax high limit of the axis
2486 */
2487 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
2488 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
2489
2490 ctx.textAlign = "left";
2491 ctx.textBaseline = "top";
2492
2493 if (xmax <= xmin || width <= 0)
2494 return;
2495
2496 /* force date display if xmax not today */
2497 let d1 = new Date(xmax * 1000);
2498 let d2 = new Date();
2499 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
2500
2501 /* use 5 pixel as min tick distance */
2502 let dx = Math.round((xmax - xmin) / (width / 5));
2503
2504 let tick_base;
2505 for (tick_base = 0; base[tick_base]; tick_base++) {
2506 if (base[tick_base] > dx)
2507 break;
2508 }
2509 if (!base[tick_base])
2510 tick_base--;
2511 dx = base[tick_base];
2512
2513 let major_base = tick_base;
2514 let major_dx = dx;
2515
2516 let label_base = major_base;
2517 let label_dx = dx;
2518
2519 do {
2520 let str = timeToLabel(xmin, label_dx, forceDate);
2521 let maxWidth = ctx.measureText(str).width;
2522
2523 /* increasing label_dx, if labels would overlap */
2524 if (maxWidth > 0.75 * label_dx / (xmax - xmin) * width) {
2525 if (base[label_base + 1])
2526 label_dx = base[++label_base];
2527 else
2528 label_dx += 3600 * 24;
2529
2530 if (label_base > major_base + 1 || !base[label_base + 1]) {
2531 if (base[major_base + 1])
2532 major_dx = base[++major_base];
2533 else
2534 major_dx += 3600 * 24;
2535 }
2536
2537 if (major_base > tick_base + 1 || !base[label_base + 1]) {
2538 if (base[tick_base + 1])
2539 dx = base[++tick_base];
2540 else
2541 dx += 3600 * 24;
2542 }
2543
2544 } else
2545 break;
2546 } while (1);
2547
2548 let d = new Date(xmin * 1000);
2549 let tz = d.getTimezoneOffset() * 60;
2550
2551 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
2552
2553 ctx.strokeStyle = this.param.color.axis;
2554 ctx.drawLine(x1, y1, x1 + width, y1);
2555
2556 do {
2557 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
2558
2559 if (xs > x1 + width + 0.001)
2560 break;
2561
2562 if (xs >= x1) {
2563 if ((x_act - tz) % major_dx === 0) {
2564 if ((x_act - tz) % label_dx === 0) {
2565 // label tick mark
2566 ctx.strokeStyle = this.param.color.axis;
2567 ctx.drawLine(xs, y1, xs, y1 + text);
2568
2569 // grid line
2570 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2571 ctx.strokeStyle = this.param.color.grid;
2572 ctx.drawLine(xs, y1, xs, y1 + grid);
2573 }
2574
2575 // label
2576 if (label !== 0) {
2577 let str = timeToLabel(x_act, label_dx, forceDate);
2578
2579 // if labels at edge, shift them in
2580 let xl = xs - ctx.measureText(str).width / 2;
2581 if (xl < 0)
2582 xl = 0;
2583 if (xl + ctx.measureText(str).width >= xr)
2584 xl = xr - ctx.measureText(str).width - 1;
2585 ctx.strokeStyle = this.param.color.label;
2586 ctx.fillStyle = this.param.color.label;
2587 ctx.fillText(str, xl, y1 + label);
2588 }
2589 } else {
2590 // major tick mark
2591 ctx.strokeStyle = this.param.color.axis;
2592 ctx.drawLine(xs, y1, xs, y1 + major);
2593 }
2594
2595 // grid line
2596 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2597 ctx.strokeStyle = this.param.color.grid;
2598 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2599 }
2600 } else {
2601 // minor tick mark
2602 ctx.strokeStyle = this.param.color.axis;
2603 ctx.drawLine(xs, y1, xs, y1 + minor);
2604 }
2605 }
2606
2607 x_act += dx;
2608
2609 } while (1);
2610};
2611
2612MPlotGraph.prototype.drawCAxis = function (ctx, x1, y1, width, tick, label, category, draw) {
2613 /** Draw the xaxis as time
2614 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2615 * @param {number} x1 coordinate position of the axis on screen
2616 * @param {number} y1 coordinate position of the axis on screen
2617 * @param {number} width width of the axis, likely also the width of the plot
2618 * @param {number} tick height in pixel of tick markers at labels
2619 * @param {number} label distance of text from axis
2620 * @param {string} category array of category labels to plot
2621 * @param {bool} draw if false, only return height of labels
2622 */
2623 ctx.textAlign = "center";
2624 ctx.textBaseline = "middle";
2625
2626 if (width <= 0)
2627 return;
2628
2629 ctx.strokeStyle = this.param.color.axis;
2630
2631 if (draw)
2632 ctx.drawLine(x1, y1, x1 + width, y1);
2633
2634 let dx = width/category.length;
2635
2636 let maxWidth;
2637 let maxHeight;
2638 let angle;
2639 for (angle = 0 ; angle < 90 ; angle += 10) {
2640 maxWidth = 0;
2641 maxHeight = 0;
2642 for (let i = 0; i < category.length; i++) {
2643
2644 // tick
2645 if (draw)
2646 ctx.drawLine(x1 + dx * (i + 0.5), y1, x1 + dx * (i + 0.5), y1 + tick);
2647
2648 // label
2649 let w = ctx.measureText(category[i]).width;
2650 let h = this.param.xAxis.textSize;
2651
2652 const cos = Math.cos(angle/180*Math.PI);
2653 const sin = Math.sin(angle/180*Math.PI);
2654
2655 const width = Math.abs(w * cos) + Math.abs(h * sin);
2656 const height = Math.abs(w * sin) + Math.abs(h * cos);
2657
2658 maxWidth = Math.max(maxWidth, width);
2659 maxHeight = Math.max(maxHeight, height);
2660 }
2661
2662 if (maxWidth * 1.1 < dx)
2663 break;
2664 }
2665
2666 this.param.xAxis.angle = angle;
2667 if (draw) {
2668 for (let i = 0; i < category.length; i++) {
2669 ctx.save();
2670 ctx.translate(x1 + dx * (i + 0.5), y1 + label + maxHeight / 2);
2671 ctx.rotate(-angle / 180 * Math.PI);
2672 ctx.fillText(category[i], 0, 0);
2673 ctx.restore();
2674 }
2675 }
2676
2677 return maxHeight + 2;
2678};
2679
2680MPlotGraph.prototype.download = function (mode) {
2681 /**
2682 * Download the figure as an image or data
2683 * @param {string} mode either "CSV" | "PNG"
2684 */
2685
2686 let d = new Date();
2687 let filename = this.param.title.text + "-" +
2688 d.getFullYear() +
2689 ("0" + (d.getUTCMonth() + 1)).slice(-2) +
2690 ("0" + d.getUTCDate()).slice(-2) + "-" +
2691 ("0" + d.getUTCHours()).slice(-2) +
2692 ("0" + d.getUTCMinutes()).slice(-2) +
2693 ("0" + d.getUTCSeconds()).slice(-2);
2694
2695 // use trick from FileSaver.js
2696 let a = document.getElementById('downloadHook');
2697 if (a === null) {
2698 a = document.createElement("a");
2699 a.style.display = "none";
2700 a.id = "downloadHook";
2701 document.body.appendChild(a);
2702 }
2703
2704 if (mode === "CSV") {
2705 filename += ".csv";
2706
2707 let data = "";
2708
2709 // title
2710 this.param.plot.forEach(p => {
2711 if (p.type === "scatter" || p.type === "histogram") {
2712 data += "X,";
2713 if (p.label === "")
2714 data += "Y";
2715 else
2716 data += p.label;
2717 data += '\n';
2718
2719 // data
2720 for (let i = 0; i < p.xData.length; i++) {
2721 data += p.xData[i] + ",";
2722 data += p.yData[i] + "\n";
2723 }
2724 data += '\n';
2725 }
2726
2727 if (p.type === "colormap") {
2728 data += "X \ Y,";
2729
2730 // X-header
2731 for (let i = 0; i < p.nx; i++)
2732 data += p.xData[i] + ",";
2733 data += '\n';
2734
2735 for (let j = 0; j < p.ny; j++) {
2736 data += p.yData[j] + ",";
2737 for (let i = 0; i < p.nx; i++)
2738 data += p.zData[i + j * p.nx] + ",";
2739 data += '\n';
2740 }
2741 }
2742 });
2743
2744 let blob = new Blob([data], {type: "text/csv"});
2745 let url = window.URL.createObjectURL(blob);
2746
2747 a.href = url;
2748 a.download = filename;
2749 a.click();
2750 window.URL.revokeObjectURL(url);
2751 dlgAlert("Data downloaded to '" + filename + "'");
2752
2753 } else if (mode === "PNG") {
2754 filename += ".png";
2755
2756 let smb = this.param.showMenuButtons;
2757 this.param.showMenuButtons = false;
2758 this.draw();
2759
2760 let h = this;
2761 this.canvas.toBlob(function (blob) {
2762 let url = window.URL.createObjectURL(blob);
2763
2764 a.href = url;
2765 a.download = filename;
2766 a.click();
2767 window.URL.revokeObjectURL(url);
2768 dlgAlert("Image downloaded to '" + filename + "'");
2769
2770 h.param.showMenuButtons = smb;
2771 h.redraw();
2772
2773 }, 'image/png');
2774 }
2775
2776};
2777
2778MPlotGraph.prototype.drawTextBox = function (ctx, text, x, y) {
2779 /**
2780 * Draw a box encapsulating some text. Width and height are set by the text
2781 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2782 * @param {string} text text to include in the box
2783 * @param {number} x coordinate of box lower left corner
2784 * @param {number} y coordinate of box lower left corner
2785 * @returns null
2786 */
2787 let line = text.split("\n");
2788
2789 let mw = 0;
2790 for (const p of line)
2791 if (ctx.measureText(p).width > mw)
2792 mw = ctx.measureText(p).width;
2793 let w = 5 + mw + 5;
2794 let h = parseInt(ctx.font) * 1.5;
2795
2796 let c = ctx.fillStyle;
2797 ctx.fillStyle = "white";
2798 ctx.fillRect(x, y, w, h * line.length);
2799 ctx.fillStyle = c;
2800 ctx.strokeRect(x, y, w, h * line.length);
2801
2802 for (let i=0 ; i<line.length ; i++)
2803 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
2804}
2805
2806MPlotGraph.prototype.mouseEvent = function (e) {
2807 /**
2808 * Handle mouse events
2809 * @param {Object} e mouse event object, specifies type, buttons
2810 */
2811
2812 // execute callback if registered
2813 if (this.param.event) {
2814
2815 if (this.param.plot[0].type === "colormap") {
2816 // pass plot column/row to callback
2817 let x = this.screenToX(e.offsetX);
2818 let y = this.screenToY(e.offsetY);
2819 let xMin = this.param.plot[0].xMin;
2820 let xMax = this.param.plot[0].xMax;
2821 let yMin = this.param.plot[0].yMin;
2822 let yMax = this.param.plot[0].yMax;
2823 let dx = (xMax - xMin) / this.nx;
2824 let dy = (yMax - yMin) / this.ny;
2825 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
2826 x > xMin && x < xMax && y > yMin && y < yMax) {
2827 let ix = Math.floor((x - xMin) / dx);
2828 let iy = Math.floor((y - yMin) / dy);
2829
2830 let flag = eval(this.param.event + "(e, this, ix, iy)");
2831 if (flag)
2832 return;
2833 }
2834 } else {
2835
2836 // call all other plots only with event and object
2837 let flag = eval(this.param.event + "(e, this)");
2838 if (flag)
2839 return;
2840
2841 }
2842 }
2843
2844 // fix buttons for IE
2845 if (!e.which && e.button) {
2846 if ((e.button & 1) > 0) e.which = 1; // Left
2847 else if ((e.button & 4) > 0) e.which = 2; // Middle
2848 else if ((e.button & 2) > 0) e.which = 3; // Right
2849 }
2850
2851 let cursor = "default";
2852 let title = "";
2853 let cancel = false;
2854
2855 // cancel dragging in case we did not catch the mouseup event
2856 if (e.type === "mousemove" && e.buttons === 0 &&
2857 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
2858 cancel = true;
2859
2860 if (e.type === "mousedown") {
2861
2862 this.downloadSelector.style.display = "none";
2863
2864 // check for buttons
2865 this.button.forEach(b => {
2866 if (e.offsetX > b.x1 && e.offsetX < b.x1 + b.width &&
2867 e.offsetY > b.y1 && e.offsetY < b.y1 + b.width &&
2868 b.enabled) {
2869 b.click(this);
2870 }
2871 });
2872
2873 // check for dragging
2874 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2875 e.offsetY > this.y2 && e.offsetY < this.y1) {
2876 this.drag.active = true;
2877 this.marker.active = false;
2878 this.drag.sxStart = e.offsetX;
2879 this.drag.syStart = e.offsetY;
2880 this.drag.xStart = this.screenToX(e.offsetX);
2881 this.drag.yStart = this.screenToY(e.offsetY);
2882 this.drag.xMinStart = this.xMin;
2883 this.drag.xMaxStart = this.xMax;
2884 this.drag.yMinStart = this.yMin;
2885 this.drag.yMaxStart = this.yMax;
2886
2887 this.blockAutoScale = true;
2888 }
2889
2890 // check for axis dragging
2891 if (e.offsetX > this.x1 && e.offsetX < this.x2 && e.offsetY > this.y1) {
2892 this.zoom.x.active = true;
2893 this.zoom.x.x1 = e.offsetX;
2894 this.zoom.x.x2 = undefined;
2895 this.zoom.x.t1 = this.screenToX(e.offsetX);
2896 }
2897 if (e.offsetY < this.y1 && e.offsetY > this.y2 && e.offsetX < this.x1) {
2898 this.zoom.y.active = true;
2899 this.zoom.y.y1 = e.offsetY;
2900 this.zoom.y.y2 = undefined;
2901 this.zoom.y.v1 = this.screenToY(e.offsetY);
2902 }
2903
2904 } else if (cancel || e.type === "mouseup") {
2905
2906 if (this.drag.active)
2907 this.drag.active = false;
2908
2909 if (this.zoom.x.active) {
2910 if (this.zoom.x.x2 !== undefined &&
2911 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
2912 let x1 = this.zoom.x.t1;
2913 let x2 = this.screenToX(this.zoom.x.x2);
2914 if (x1 > x2)
2915 [x1, x2] = [x2, x1];
2916 this.xMin = x1;
2917 this.xMax = x2;
2918 }
2919 this.zoom.x.active = false;
2920 this.blockAutoScale = true;
2921 this.redraw();
2922 }
2923
2924 if (this.zoom.y.active) {
2925 if (this.zoom.y.y2 !== undefined &&
2926 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
2927 let y1 = this.zoom.y.v1;
2928 let y2 = this.screenToY(this.zoom.y.y2);
2929 if (y1 > y2)
2930 [y1, y2] = [y2, y1];
2931 this.yMin = y1;
2932 this.yMax = y2;
2933 }
2934 this.zoom.y.active = false;
2935 this.blockAutoScale = true;
2936 this.redraw();
2937 }
2938
2939 } else if (e.type === "mousemove") {
2940
2941 if (this.drag.active) {
2942
2943 // execute dragging
2944 cursor = "move";
2945
2946 if (this.param.xAxis.log) {
2947 let dx = e.offsetX - this.drag.sxStart;
2948
2949 this.xMin = Math.exp(((this.x1 - dx) - this.x1) / (this.x2 - this.x1) * (Math.log(this.drag.xMaxStart)-Math.log(this.drag.xMinStart)) + Math.log(this.drag.xMinStart));
2950 this.xMax = Math.exp(((this.x2 - dx) - this.x1) / (this.x2 - this.x1) * (Math.log(this.drag.xMaxStart)-Math.log(this.drag.xMinStart)) + Math.log(this.drag.xMinStart));
2951
2952 if (this.xMin <= 0)
2953 this.xMin = 1E-20;
2954 if (this.xMax <= 0)
2955 this.xMax = 1E-18;
2956 } else {
2957 let dx = (e.offsetX - this.drag.sxStart) / (this.x2 - this.x1) * (this.xMax - this.xMin);
2958 this.xMin = this.drag.xMinStart - dx;
2959 this.xMax = this.drag.xMaxStart - dx;
2960 }
2961
2962 if (this.param.yAxis.log) {
2963 let dy = e.offsetY - this.drag.syStart;
2964
2965 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));
2966 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));
2967
2968 if (this.yMin <= 0)
2969 this.yMin = 1E-20;
2970 if (this.yMax <= 0)
2971 this.yMax = 1E-18;
2972 } else {
2973 let dy = (this.drag.syStart - e.offsetY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
2974 this.yMin = this.drag.yMinStart - dy;
2975 this.yMax = this.drag.yMaxStart - dy;
2976 }
2977
2978 this.redraw();
2979
2980 } else {
2981
2982 // change cursor to pointer over buttons
2983 this.button.forEach(b => {
2984 if (e.offsetX > b.x1 && e.offsetY > b.y1 &&
2985 e.offsetX < b.x1 + b.width && e.offsetY < b.y1 + b.height) {
2986 cursor = "pointer";
2987 title = b.title;
2988 }
2989 });
2990
2991 // execute axis zoom
2992 if (this.zoom.x.active) {
2993 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, e.offsetX));
2994 this.zoom.x.t2 = this.screenToX(e.offsetX);
2995 this.redraw();
2996 }
2997 if (this.zoom.y.active) {
2998 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, e.offsetY));
2999 this.zoom.y.v2 = this.screenToY(e.offsetY);
3000 this.redraw();
3001 }
3002
3003 // check if cursor close to plot point
3004 if (this.param.plot[0].type === "scatter" || this.param.plot[0].type === "histogram") {
3005 let minDist = 10000;
3006 for (const [pi, p] of this.param.plot.entries()) {
3007 if (p.xData === undefined || p.xData === null)
3008 continue;
3009
3010 for (let i = 0; i < p.xData.length; i++) {
3011 let x = this.xToScreen(p.xData[i]);
3012 let y = this.yToScreen(p.yData[i]);
3013 let d = (e.offsetX - x) * (e.offsetX - x) +
3014 (e.offsetY - y) * (e.offsetY - y);
3015 if (d < minDist) {
3016 minDist = d;
3017 this.marker.x = p.xData[i];
3018 this.marker.y = p.yData[i];
3019 this.marker.sx = x;
3020 this.marker.sy = y;
3021 this.marker.mx = e.offsetX;
3022 this.marker.my = e.offsetY;
3023 this.marker.plotIndex = pi;
3024 this.marker.index = i;
3025 }
3026 }
3027 }
3028
3029 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
3030 }
3031
3032 if (this.param.plot[0].type === "colormap") {
3033 let x = this.screenToX(e.offsetX);
3034 let y = this.screenToY(e.offsetY);
3035 let xMin = this.param.plot[0].xMin;
3036 let xMax = this.param.plot[0].xMax;
3037 let yMin = this.param.plot[0].yMin;
3038 let yMax = this.param.plot[0].yMax;
3039 let dx = (xMax - xMin) / this.nx;
3040 let dy = (yMax - yMin) / this.ny;
3041 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
3042 x > xMin && x < xMax && y > yMin && y < yMax) {
3043 let i = Math.floor((x - xMin) / dx);
3044 let j = Math.floor((y - yMin) / dy);
3045
3046 this.marker.x = (i + 0.5) * dx + xMin;
3047 this.marker.y = (j + 0.5) * dy + yMin;
3048 this.marker.z = this.param.plot[0].zData[i + j * this.nx];
3049
3050 this.marker.sx = this.xToScreen(this.marker.x);
3051 this.marker.sy = this.yToScreen(this.marker.y);
3052 this.marker.mx = e.offsetX;
3053 this.marker.my = e.offsetY;
3054 this.marker.plotIndex = 0;
3055 this.marker.active = true;
3056 } else {
3057 this.marker.active = false;
3058 }
3059 }
3060
3061 this.draw();
3062 }
3063
3064 } else if (e.type === "wheel") {
3065
3066 let x = this.screenToX(e.offsetX);
3067 let y = this.screenToY(e.offsetY);
3068 // Guard against scale <= -1 otherwise this.xMin becomes larger than this.xMax
3069 let scale = Math.max(e.deltaY * 0.01, -0.9);
3070
3071 let xMinOld = this.xMin;
3072 let xMaxOld = this.xMax;
3073 let yMinOld = this.yMin;
3074 let yMaxOld = this.yMax;
3075
3076 if (this.param.xAxis.log) {
3077
3078 scale *= 10;
3079 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
3080
3081 this.xMax *= 1 + scale * (1 - f);
3082 this.xMin /= 1 + scale * f;
3083
3084 if (this.xMax <= this.xMin) {
3085 this.xMin = xMinOld;
3086 this.xMax = xMaxOld;
3087 }
3088
3089 } else {
3090 let dx = (this.xMax - this.xMin) * scale;
3091 let f = (x - this.xMin) / (this.xMax - this.xMin);
3092 this.xMin = this.xMin - dx * f;
3093 this.xMax = this.xMax + dx * (1 - f);
3094 }
3095
3096 // avoid too high zoom (would kill axis rendering)
3097 if (this.xMax - this.xMin < 1E-10*(this.xMax0 - this.xMin0)) {
3098 this.xMin = xMinOld;
3099 this.xMax = xMaxOld;
3100 }
3101
3102 if (this.param.yAxis.log) {
3103
3104 scale *= 10;
3105 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
3106 let yMinOld = this.yMin;
3107 let yMaxOld = this.yMax;
3108
3109 this.yMax *= 1 + scale * f;
3110 this.yMin /= 1 + scale * (1 - f);
3111
3112 if (this.yMax <= this.yMin) {
3113 this.yMin = yMinOld;
3114 this.yMax = yMaxOld;
3115 }
3116
3117 } else {
3118 let dy = (this.yMax - this.yMin) * scale;
3119 let f = (y - this.yMin) / (this.yMax - this.yMin);
3120 this.yMin = this.yMin - dy * f;
3121 this.yMax = this.yMax + dy * (1 - f);
3122 }
3123
3124 // avoid too high zoom (would kill axis rendering)
3125 if (this.yMax - this.yMin < 1E-10*(this.yMax0 - this.yMin0)) {
3126 this.yMin = yMinOld;
3127 this.yMax = yMaxOld;
3128 }
3129
3130 this.blockAutoScale = true;
3131
3132 this.draw();
3133 }
3134
3135
3136 this.parentDiv.title = title;
3137 this.parentDiv.style.cursor = cursor;
3138
3139 e.preventDefault();
3140}
3141
3142MPlotGraph.prototype.resetAxes = function () {
3143 /**
3144 * Reset min/max of x and y axes, redraws
3145 */
3146 this.xMin = this.xMin0;
3147 this.xMax = this.xMax0;
3148 this.yMin = this.yMin0;
3149 this.yMax = this.yMax0;
3150
3151 this.blockAutoScale = false;
3152
3153 this.redraw();
3154}