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 log: false,
43 min: undefined,
44 max: undefined,
45 grid: true,
46 textSize: 20,
47 title: {
48 text: "",
49 textSize : 20
50 }
51 },
52
53 yAxis: {
54 log: false,
55 min: undefined,
56 max: undefined,
57 grid: true,
58 textSize: 20,
59 title: {
60 text: "",
61 textSize : 20
62 }
63 },
64
65 zAxis: {
66 show: true,
67 min: undefined,
68 max: undefined,
69 textSize: 14,
70 title: {
71 text: "",
72 textSize : 20
73 }
74 },
75
76 plot: [
77 {
78 type: "scatter",
79 odbPath: "",
80 x: "",
81 y: "",
82 label: "",
83 alpha: 1,
84
85 marker: {
86 draw: true,
87 lineColor: 0,
88 fillColor: 0,
89 style: "circle",
90 size: 10,
91 lineWidth: 2
92 },
93
94 line: {
95 draw: true,
96 fill: false,
97 color: 0,
98 style: "solid",
99 width: 2
100 }
101 },
102 ],
103};
104
105function mplot_init() {
106 // go through all data-name="mplot" tags
107 let mPlot = document.getElementsByClassName("mplot");
108
109 for (let i = 0; i < mPlot.length; i++)
110 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
111
112 loadMPlotData();
113
114 window.addEventListener('resize', windowResize);
115}
116
117function profile(flag) {
118 if (flag === true || flag === undefined) {
119 console.log("");
120 profile.startTime = new Date().getTime();
121 return;
122 }
123
124 let now = new Date().getTime();
125 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
126 profile.startTime = new Date().getTime();
127}
128
129function windowResize() {
130 let mPlot = document.getElementsByClassName("mplot");
131 for (const m of mPlot)
132 m.mpg.resize();
133}
134
135function isObject(item) {
136 return (item && typeof item === 'object' && !Array.isArray(item));
137}
138
139function deepMerge(target, source) {
140 for (let key in source) {
141 if (source.hasOwnProperty(key)) {
142 if (isObject(source[key])) {
143 if (!target[key]) Object.assign(target, { [key]: {} });
144 deepMerge(target[key], source[key]);
145 } else {
146 Object.assign(target, { [key]: source[key] });
147 }
148 }
149 }
150 return target;
151}
152
153function MPlotGraph(divElement, param) { // Constructor
154
155 // save parameters from <div>
156 this.parentDiv = divElement;
157 this.divParam = divElement.innerHTML;
158 divElement.innerHTML = "";
159
160 // if absent, generate random string (5 char) to give an id to parent element
161 if (!this.parentDiv.id)
162 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
163
164 // default parameters
165 this.param = JSON.parse(JSON.stringify(defaultParam)); // deep copy
166
167 // overwrite default parameters from <div> text body
168 try {
169 if (this.divParam.includes('{')) {
170 let p = JSON.parse(this.divParam);
171 this.param = deepMerge(this.param, p);
172 }
173 } catch (error) {
174 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
175 dlgAlert(error);
176 return;
177 }
178
179 // obtain parameters form <div> attributes ---
180
181 // data-odb-path
182 if (this.parentDiv.dataset.odbPath)
183 for (let p of this.param.plot)
184 p.odbPath = this.parentDiv.dataset.odbPath;
185
186 // data-title
187 if (this.parentDiv.dataset.title)
188 this.param.title.text = this.parentDiv.dataset.title;
189
190 // data-x/y/z-text
191 if (this.parentDiv.dataset.xText)
192 this.param.xAxis.title.text =this.parentDiv.dataset.xText;
193 if (this.parentDiv.dataset.yText)
194 this.param.yAxis.title.text =this.parentDiv.dataset.yText;
195 if (this.parentDiv.dataset.zText)
196 this.param.zAxis.title.text =this.parentDiv.dataset.zText;
197
198 // data-x/y
199 if (this.parentDiv.dataset.x)
200 this.param.plot[0].x = this.parentDiv.dataset.x;
201 if (this.parentDiv.dataset.y)
202 this.param.plot[0].y = this.parentDiv.dataset.y;
203
204 // data-x/y/z-min/max
205 if (this.parentDiv.dataset.xMin)
206 this.param.xAxis.min = parseFloat(this.parentDiv.dataset.xMin);
207 if (this.parentDiv.dataset.xMax)
208 this.param.xAxis.max = parseFloat(this.parentDiv.dataset.xMax);
209 if (this.parentDiv.dataset.yMin)
210 this.param.yAxis.min = parseFloat(this.parentDiv.dataset.yMin);
211 if (this.parentDiv.dataset.yMax)
212 this.param.yAxis.max = parseFloat(this.parentDiv.dataset.yMax);
213 if (this.parentDiv.dataset.zMin)
214 this.param.zAxis.min = parseFloat(this.parentDiv.dataset.zMin);
215 if (this.parentDiv.dataset.zMax)
216 this.param.zAxis.max = parseFloat(this.parentDiv.dataset.zMax);
217
218 // data-x/y/z-log
219 if (this.parentDiv.dataset.xLog)
220 this.param.xAxis.log = this.parentDiv.dataset.xLog === "true" || this.parentDiv.dataset.xLog === "1";
221 if (this.parentDiv.dataset.yLog)
222 this.param.yAxis.log = this.parentDiv.dataset.yLog === "true" || this.parentDiv.dataset.yLog === "1";
223 if (this.parentDiv.dataset.zLog) {
224 this.param.zAxis.log = this.parentDiv.dataset.zLog === "true" || this.parentDiv.dataset.zLog === "1";
225 if (this.param.zAxis.log) {
226 if (this.param.zAxis.min < 1E-20)
227 this.param.zAxis.min = 1E-20;
228 if (this.param.zAxis.max < 1E-18)
229 this.param.zAxis.max = 1E-18;
230 }
231 }
232
233 // data-h
234 if (this.parentDiv.dataset.h) {
235 this.param.plot[0].type = "histogram";
236 this.param.plot[0].y = this.parentDiv.dataset.h;
237 this.param.plot[0].line.color = "#404040";
238 if (!this.parentDiv.dataset.x) {
239 this.param.plot[0].xMin = this.param.xAxis.min;
240 this.param.plot[0].xMax = this.param.xAxis.max;
241 }
242 }
243
244 // data-z
245 if (this.parentDiv.dataset.z) {
246 this.param.plot[0].type = "colormap";
247 this.param.plot[0].showZScale = true;
248 this.param.plot[0].bgcolor = this.parentDiv.dataset.bgcolor;
249 this.param.plot[0].z = this.parentDiv.dataset.z;
250 this.param.plot[0].xMin = this.param.xAxis.min;
251 this.param.plot[0].xMax = this.param.xAxis.max;
252 this.param.plot[0].yMin = this.param.yAxis.min;
253 this.param.plot[0].yMax = this.param.yAxis.max;
254 this.param.plot[0].zMin = this.param.zAxis.min;
255 this.param.plot[0].zMax = this.param.zAxis.max;
256 this.param.plot[0].nx = parseInt(this.parentDiv.dataset.nx);
257 this.param.plot[0].ny = parseInt(this.parentDiv.dataset.ny);
258 if (this.param.plot[0].nx === undefined) {
259 dlgAlert("\"data-nx\" missing for colormap mplot <div>");
260 return;
261 }
262 if (this.param.plot[0].ny === undefined) {
263 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
264 return;
265 }
266 }
267
268 // data-x<n>/y<n>/label<n>/alpha<n>
269 for (let i=0 ; i<16 ; i++) {
270 let index = 0;
271 if (this.parentDiv.dataset["x"+i]) {
272 if (this.param.plot[0].x !== "") {
273 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
274 index = this.param.plot.length-1;
275 this.param.plot[index].marker.lineColor = index;
276 this.param.plot[index].marker.fillColor = index;
277 this.param.plot[index].line.color = index;
278 }
279 this.param.plot[index].x = this.parentDiv.dataset["x" + i];
280 }
281 if (this.parentDiv.dataset["y"+i])
282 this.param.plot[index].y = this.parentDiv.dataset["y"+i];
283 if (this.parentDiv.dataset["label"+i])
284 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
285 if (this.parentDiv.dataset["alpha"+i])
286 this.param.plot[index].alpha = parseFloat(this.parentDiv.dataset["alpha"+i]);
287 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
288 }
289
290 // data-h<n>
291 for (let i=0 ; i<16 ; i++) {
292 let index = 0;
293 if (this.parentDiv.dataset["h"+i]) {
294 if (this.param.plot[0].y !== "") {
295 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
296 index = this.param.plot.length-1;
297 this.param.plot[index].marker.lineColor = index;
298 this.param.plot[index].marker.fillColor = index;
299 this.param.plot[index].line.color = index;
300 }
301 this.param.plot[index].type = "histogram";
302 this.param.plot[index].y = this.parentDiv.dataset["h" + i];
303
304 this.param.plot[index].xMin = this.param.xAxis.min;
305 this.param.plot[index].xMax = this.param.xAxis.max;
306
307 if (this.parentDiv.dataset["label"+i])
308 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
309 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
310 }
311 }
312
313 // data-overlay
314 if (this.parentDiv.dataset.overlay) {
315 this.param.overlay = this.parentDiv.dataset.overlay;
316 if (this.param.overlay.indexOf('(') !== -1) // strip any '('
317 this.param.overlay = this.param.overlay.substring(0, this.param.overlay.indexOf('('));
318 }
319
320 // data-event
321 if (this.parentDiv.dataset.event) {
322 this.param.event = this.parentDiv.dataset.event;
323 if (this.param.event.indexOf('(') !== -1) // strip any '('
324 this.param.event = this.param.event.substring(0, this.param.event.indexOf('('));
325 }
326
327 // set parameters from constructor
328 if (param) {
329 this.param.plot[0] = deepMerge(this.param.plot[0], param);
330 if (this.param.plot[0].type === "colormap") {
331 this.calcMinMax();
332
333 if (this.param.plot[0].nx === undefined) {
334 dlgAlert("\"nx\" missing in param for colormap mplot <div>");
335 return;
336 }
337 if (this.param.plot[0].ny === undefined) {
338 dlgAlert("\"ny\" missing in param for colormap mplot <div>");
339 return;
340 }
341 }
342 }
343
344 // dragging
345 this.drag = {
346 active: false,
347 sxStart: 0,
348 syStart: 0,
349 xStart: 0,
350 yStart: 0,
351 xMinStart: 0,
352 xMaxStart: 0,
353 yMinStart: 0,
354 yMaxStart: 0,
355 };
356
357 // axis zoom
358 this.zoom = {
359 x: {active: false},
360 y: {active: false}
361 };
362
363 // marker
364 this.marker = {active: false};
365 this.blockAutoScale = false;
366
367 this.error = null;
368
369 // buttons
370 this.button = [
371 {
372 src: "rotate-ccw.svg",
373 title: "Reset histogram axes",
374 click: function (t) {
375 t.resetAxes();
376 }
377 },
378 {
379 src: "download.svg",
380 title: "Download image/data...",
381 click: function (t) {
382 if (t.downloadSelector.style.display === "none") {
383 t.downloadSelector.style.display = "block";
384 let w = t.downloadSelector.getBoundingClientRect().width;
385 t.downloadSelector.style.left = (t.canvas.getBoundingClientRect().x + window.scrollX +
386 t.width - 26 - w) + "px";
387 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.scrollY +
388 this.y1) + "px";
389 t.downloadSelector.style.zIndex = "32";
390 } else {
391 t.downloadSelector.style.display = "none";
392 }
393 }
394 },
395 ];
396
397 this.button.forEach(b => {
398 b.img = new Image();
399 b.img.src = "icons/" + b.src;
400 });
401
402 this.createDownloadSelector();
403
404 // mouse event handlers
405 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
406 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
407 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
408 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
409 divElement.addEventListener("wheel", this.mouseEvent.bind(this), true);
410
411 // Keyboard event handler (has to be on the window!)
412 window.addEventListener("keydown", this.keyDown.bind(this));
413
414 // create canvas
415 this.canvas = document.createElement("canvas");
416 this.canvas.style.border = "1px solid black";
417
418 if (parseInt(this.parentDiv.style.width) > 0)
419 this.canvas.width = parseInt(this.parentDiv.style.width);
420 else
421 this.canvas.width = 500;
422 if (parseInt(this.parentDiv.style.height) > 0)
423 this.canvas.height = parseInt(this.parentDiv.style.height);
424 else
425 this.canvas.height = 300;
426
427 divElement.appendChild(this.canvas);
428}
429
430MPlotGraph.prototype.createDownloadSelector = function () {
431 // download selector
432 let downloadSelId = this.parentDiv.id + "downloadSel";
433 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
434 this.downloadSelector = document.createElement("div");
435 this.downloadSelector.id = downloadSelId;
436 this.downloadSelector.style.display = "none";
437 this.downloadSelector.style.position = "absolute";
438 this.downloadSelector.className = "mtable";
439 this.downloadSelector.style.borderRadius = "0";
440 this.downloadSelector.style.border = "2px solid #808080";
441 this.downloadSelector.style.margin = "0";
442 this.downloadSelector.style.padding = "0";
443
444 this.downloadSelector.style.left = "100px";
445 this.downloadSelector.style.top = "100px";
446
447 let table = document.createElement("table");
448 let mhg = this;
449
450 let row = document.createElement("tr");
451 let cell = document.createElement("td");
452 cell.style.padding = "0";
453 let link = document.createElement("a");
454 link.href = "#";
455 link.innerHTML = "CSV";
456 link.title = "Download data in Comma Separated Value format";
457 link.onclick = function () {
458 mhg.downloadSelector.style.display = "none";
459 mhg.download("CSV");
460 return false;
461 }.bind(this);
462 cell.appendChild(link);
463 row.appendChild(cell);
464 table.appendChild(row);
465
466 row = document.createElement("tr");
467 cell = document.createElement("td");
468 cell.style.padding = "0";
469 link = document.createElement("a");
470 link.href = "#";
471 link.innerHTML = "PNG";
472 link.title = "Download image in PNG format";
473 link.onclick = function () {
474 mhg.downloadSelector.style.display = "none";
475 mhg.download("PNG");
476 return false;
477 }.bind(this);
478 cell.appendChild(link);
479 row.appendChild(cell);
480 table.appendChild(row);
481
482 this.downloadSelector.appendChild(table);
483 document.body.appendChild(this.downloadSelector);
484}
485
486MPlotGraph.prototype.keyDown = function (e) {
487
488 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
489 this.resetAxes();
490 e.preventDefault();
491 }
492}
493
494function loadMPlotData() {
495
496 // go through all data-name="mplot" tags
497 let mPlot = document.getElementsByClassName("mplot");
498
499 let v = [];
500 for (const mp of mPlot) {
501 for (const pl of mp.mpg.param.plot) {
502 if (pl.odbPath === undefined || pl.odbPath === "")
503 continue;
504
505 let name = pl.label;
506 if (name === "")
507 name = mp.id;
508
509 if ((pl.type === "scatter" || pl.type === "histogram") &&
510 (pl.y === undefined || pl.y === null || pl.y === "")) {
511 mp.mpg.error ="Invalid Y data \"" + pl.y + "\" for " + pl.type + " plot \"" + name+ "\"";
512 mp.mpg.draw();
513 pl.invalid = true;
514 continue;
515 }
516
517 if ((pl.type === "colormap") &&
518 (pl.z === undefined || pl.z === null || pl.z === "")) {
519 mp.mpg.error = "Invalid Z data \"" + pl.y + "\" for colormap plot \"" + name + "\"";
520 mp.mpg.draw();
521 pl.invalid = true;
522 continue;
523 }
524
525 if (pl.odbPath.slice(-1) !== '/')
526 pl.odbPath += '/';
527
528 if (pl.x !== undefined && pl.x !== null && pl.x !== "")
529 v.push(pl.odbPath + pl.x);
530 if (pl.y !== undefined && pl.y !== null && pl.y !== "")
531 v.push(pl.odbPath + pl.y);
532 if (pl.z !== undefined && pl.z !== null && pl.z !== "")
533 v.push(pl.odbPath + pl.z);
534 }
535 }
536
537 mjsonrpc_db_get_values(v).then( function(rpc) {
538
539 let mPlot = document.getElementsByClassName("mplot");
540 let i = 0;
541 for (let mp of mPlot) {
542 for (let p of mp.mpg.param.plot) {
543 if (!p.odbPath === undefined || p.odbPath === "" || p.invalid)
544 continue;
545
546 let name = p.label;
547 if (name === "")
548 name = mp.id;
549
550 if (p.x !== undefined && p.x !== null && p.x !== "") {
551 p.xData = rpc.result.data[i++];
552 if (p.xData === null)
553 mp.mpg.error = "Invalid X data \"" + p.x + "\" for scatter plot \"" + name + "\"";
554 }
555 if (p.y !== undefined && p.y !== null && p.y !== "") {
556 p.yData = rpc.result.data[i++];
557 if (p.yData === null)
558 mp.mpg.error = "Invalid Y data \"" + p.y + "\" for scatter plot \"" + name + "\"";
559 }
560 if (p.z !== undefined && p.z !== null && p.z !== "") {
561 p.zData = rpc.result.data[i++];
562 if (p.zData === null)
563 mp.mpg.error = "Invalid Z data \"" + p.z + "\" for scatter plot \"" + name + "\"";
564 }
565
566 if ((p.type === "scatter" || p.type === "histogram") && mp.mpg.error === null) {
567 // generate X data for histograms
568 if (p.xData === undefined || p.xData === null) {
569
570 if (p.type === "scatter") {
571 // scatter plot goes from 0 ... N
572 p.xMin = 0;
573 p.xMax = p.yData.length;
574 p.xData = Array.from({length: p.yData.length}, (v, i) => i);
575 } else {
576 // histogram goes from -0.5 ... N-0.5 to have bins centered over bin x-value
577 p.xMin = -0.5;
578 p.xMax = p.yData.length - 0.5;
579
580 let dx = (p.xMax - p.xMin) / p.yData.length;
581 let x0 = p.xMin + dx / 2;
582 p.xData = Array.from({length: p.yData.length}, (v, i) => x0 + i * dx);
583 }
584 } else {
585 if (p.xMin === undefined) {
586 p.xMin = Math.min(...p.xData);
587 p.xMax = Math.max(...p.xData);
588 }
589 }
590
591 p.yMin = Math.min(...p.yData);
592 p.yMax = Math.max(...p.yData);
593 }
594
595 if (p.type === "colormap" && mp.mpg.error === null) {
596 p.zMin = Math.min(...p.zData.filter(v=>!isNaN(v)));
597 p.zMax = Math.max(...p.zData.filter(v=>!isNaN(v)));
598
599 if (p.xMin === undefined) {
600 p.xMin = -0.5;
601 p.xMax = p.nx - 0.5;
602 }
603 if (p.yMin === undefined) {
604 p.yMin = -0.5;
605 p.yMax = p.ny - 0.5;
606 }
607
608 let dx = (p.xMax - p.xMin) / p.nx;
609 let x0 = p.xMin + dx/2;
610 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
611
612 let dy = (p.yMax - p.yMin) / p.ny;
613 let y0 = p.yMin + dy/2;
614 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
615 }
616 }
617 }
618
619 for (const mp of mPlot) {
620 if (!mp.mpg.blockAutoScale)
621 mp.mpg.calcMinMax();
622 mp.mpg.redraw();
623 }
624
625 // refresh data once per second
626 window.setTimeout(loadMPlotData, 1000);
627
628 }).catch( (error) => {
629 dlgAlert(error)
630 });
631}
632
633MPlotGraph.prototype.setData = function (index, x, y) {
634
635 if (index > this.param.plot.length) {
636 dlgAlert("Wrong index \"" + index + "\" for graph \""+ this.param.title.text +"\"<br />" +
637 "New index must be \"" + this.param.plot.length + "\"");
638 return;
639 }
640
641 let p;
642
643 if (index + 1 > this.param.plot.length) {
644 // add new default plot
645 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
646 p = this.param.plot[index];
647 p.marker.lineColor = index;
648 p.marker.fillColor = index;
649 p.line.color = index;
650 p.type = y ? "scatter" : "histogram";
651 } else
652 p = this.param.plot[index];
653
654 p.odbPath = ""; // prevent loading of ODB data
655
656 if (p.type === "colormap") {
657 p.zData = x; // 2D array of colormap plot
658
659 p.zMin = undefined;
660 p.zMax = undefined;
661 for (const value of p.zData) {
662 if (!isNaN(value)) {
663 if (typeof p.zMin === 'undefined' || p.zMin > value)
664 p.zMin = value;
665 if (typeof p.zMax === 'undefined' || p.zMax < value)
666 p.zMax = value;
667 }
668 }
669
670 if (p.xMin === undefined) {
671 p.xMin = -0.5;
672 p.xMax = p.nx - 0.5;
673 }
674 if (p.yMin === undefined) {
675 p.yMin = -0.5;
676 p.yMax = p.ny - 0.5;
677 }
678
679 let dx = (p.xMax - p.xMin) / p.nx;
680 let x0 = p.xMin + dx/2;
681 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
682
683 let dy = (p.yMax - p.yMin) / p.ny;
684 let y0 = p.yMin + dy/2;
685 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
686 }
687
688
689 if (p.type === "histogram") {
690 p.yData = x;
691 p.line.color = "#404040";
692 // generate X data for histograms
693 if (p.xMin === undefined || p.xMax === undefined) {
694 p.xMin = -0.5;
695 p.xMax = p.yData.length - 0.5;
696 }
697 let dx = (p.xMax - p.xMin) / p.yData.length;
698 let x0 = p.xMin + dx/2;
699 if (p.xData === undefined || p.xData === null)
700 p.xData = Array.from({length: p.yData.length}, (v,i) => x0 + i*dx);
701
702 p.yMin = Math.min(...p.yData);
703 p.yMax = Math.max(...p.yData);
704 }
705
706 if (p.type === "scatter" ) {
707 p.xData = x;
708 p.yData = y;
709 p.xMin = Math.min(...p.xData);
710 p.xMax = Math.max(...p.xData);
711 p.yMin = Math.min(...p.yData);
712 p.yMax = Math.max(...p.yData);
713 }
714
715 if (!this.blockAutoScale) {
716 this.calcMinMax();
717 }
718
719 this.redraw();
720}
721
722MPlotGraph.prototype.resize = function () {
723 this.canvas.width = this.parentDiv.clientWidth;
724 this.canvas.height = this.parentDiv.clientHeight;
725
726 this.redraw();
727}
728
729MPlotGraph.prototype.redraw = function () {
730 let f = this.draw.bind(this);
731 window.requestAnimationFrame(f);
732}
733
734MPlotGraph.prototype.xToScreen = function (x) {
735 if (this.param.xAxis.log) {
736 if (x <= 0)
737 return this.x1;
738 else
739 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
740 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
741 }
742 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
743}
744
745MPlotGraph.prototype.yToScreen = function (y) {
746 if (this.param.yAxis.log) {
747 if (y <= 0)
748 return this.y1;
749 else
750 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
751 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
752 }
753 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
754}
755
756MPlotGraph.prototype.screenToX = function (x) {
757 if (this.param.xAxis.log) {
758 let xl = (x - this.x1) / (this.x2 - this.x1) * (Math.log(this.xMax)-Math.log(this.xMin)) + Math.log(this.xMin);
759 return Math.exp(xl);
760 }
761 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
762};
763
764MPlotGraph.prototype.screenToY = function (y) {
765 if (this.param.yAxis.log) {
766 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
767 return Math.exp(yl);
768 }
769 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
770};
771
772MPlotGraph.prototype.calcMinMax = function () {
773
774 // simple nx / ny for colormaps
775 if (this.param.plot[0].type === "colormap") {
776 this.nx = this.param.plot[0].nx;
777 this.ny = this.param.plot[0].ny;
778
779 if (this.param.zAxis.min !== undefined)
780 this.zMin = this.param.zAxis.min;
781 else
782 this.zMin = this.param.plot[0].zMin;
783
784 if (this.param.zAxis.max !== undefined)
785 this.zMax = this.param.zAxis.max;
786 else
787 this.zMax = this.param.plot[0].zMax;
788
789 if (this.param.zAxis.log) {
790 if (this.zMin < 1E-20)
791 this.zMin = 1E-20;
792 if (this.zMax < 1E-18)
793 this.zMax = 1E-18;
794 }
795
796 this.xMin = this.param.plot[0].xMin;
797 this.xMax = this.param.plot[0].xMax;
798 this.yMin = this.param.plot[0].yMin;
799 this.yMax = this.param.plot[0].yMax;
800
801 this.xMin0 = this.xMin;
802 this.xMax0 = this.xMax;
803 this.yMin0 = this.yMin;
804 this.yMax0 = this.yMax;
805 return;
806 }
807
808 // determine min/max of overall plot
809 let xMin = this.param.plot[0].xMin;
810 for (const p of this.param.plot)
811 if (p.xMin < xMin)
812 xMin = p.xMin;
813 if (this.param.xAxis.min !== undefined)
814 xMin = this.param.xAxis.min;
815
816 let xMax = this.param.plot[0].xMax;
817 for (const p of this.param.plot)
818 if (p.xMax > xMax)
819 xMax = p.xMax;
820 if (this.param.xAxis.max !== undefined)
821 xMax = this.param.xAxis.max;
822
823 let yMin = this.param.plot[0].yMin;
824 for (const p of this.param.plot)
825 if (p.yMin < yMin)
826 yMin = p.yMin;
827 if (this.param.yAxis.min !== undefined)
828 yMin = this.param.yAxis.min;
829
830 let yMax = this.param.plot[0].yMax;
831 for (const p of this.param.plot)
832 if (p.yMax > yMax)
833 yMax = p.yMax;
834 if (this.param.yAxis.max !== undefined)
835 yMax = this.param.yAxis.max;
836
837 // avoid min === max
838 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
839 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
840
841 // add 5% on each side
842 let dx = (xMax - xMin);
843 let dy = (yMax - yMin);
844 if (this.param.plot[0].type !== "histogram") {
845 if (this.param.xAxis.min === undefined)
846 xMin -= dx / 20;
847 if (this.param.xAxis.max === undefined)
848 xMax += dx / 20;
849 if (this.param.yAxis.min === undefined)
850 yMin -= dy / 20;
851 }
852 if (this.param.yAxis.max === undefined)
853 yMax += dy / 20;
854
855 this.xMin = xMin;
856 this.xMax = xMax;
857 this.yMin = yMin;
858 this.yMax = yMax;
859
860 this.xMin0 = xMin;
861 this.xMax0 = xMax;
862 this.yMin0 = yMin;
863 this.yMax0 = yMax;
864}
865
866MPlotGraph.prototype.drawMarker = function(ctx, p, x, y) {
867 if (typeof p.marker.lineColor === "string")
868 ctx.strokeStyle = p.marker.lineColor;
869 else if (typeof p.marker.lineColor === "number")
870 ctx.strokeStyle = this.param.color.data[p.marker.lineColor];
871
872 if (typeof p.marker.fillColor === "string")
873 ctx.fillStyle = p.marker.fillColor;
874 else if (typeof p.marker.fillColor === "number")
875 ctx.fillStyle = this.param.color.data[p.marker.fillColor];
876
877 let size = p.marker.size;
878 ctx.lineWidth = p.marker.lineWidth;
879
880 switch(p.marker.style) {
881 case "circle":
882 ctx.beginPath();
883 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
884 ctx.fill();
885 ctx.stroke();
886 break;
887 case "square":
888 ctx.fillRect(x - size / 2, y - size / 2, size, size);
889 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
890 break;
891 case "diamond":
892 ctx.beginPath();
893 ctx.moveTo(x, y - size / 2);
894 ctx.lineTo(x + size / 2, y);
895 ctx.lineTo(x, y + size / 2);
896 ctx.lineTo(x - size / 2, y);
897 ctx.lineTo(x, y - size / 2);
898 ctx.fill();
899 ctx.stroke();
900 break;
901 case "pentagon":
902 ctx.beginPath();
903 ctx.moveTo(x + size * 0.00, y - size * 0.50);
904 ctx.lineTo(x + size * 0.48, y - size * 0.16);
905 ctx.lineTo(x + size * 0.30, y + size * 0.41);
906 ctx.lineTo(x - size * 0.30, y + size * 0.41);
907 ctx.lineTo(x - size * 0.48, y - size * 0.16);
908 ctx.lineTo(x + size * 0.00, y - size * 0.50);
909 ctx.fill();
910 ctx.stroke();
911 break;
912 case "triangle-up":
913 ctx.beginPath();
914 ctx.moveTo(x, y - size / 2);
915 ctx.lineTo(x + size / 2, y + size / 2);
916 ctx.lineTo(x - size / 2, y + size / 2);
917 ctx.lineTo(x, y - size / 2);
918 ctx.fill();
919 ctx.stroke();
920 break;
921 case "triangle-down":
922 ctx.beginPath();
923 ctx.moveTo(x, y + size / 2);
924 ctx.lineTo(x + size / 2, y - size / 2);
925 ctx.lineTo(x - size / 2, y - size / 2);
926 ctx.lineTo(x, y + size / 2);
927 ctx.fill();
928 ctx.stroke();
929 break;
930 case "triangle-left":
931 ctx.beginPath();
932 ctx.moveTo(x - size / 2, y);
933 ctx.lineTo(x + size / 2, y - size / 2);
934 ctx.lineTo(x + size / 2, y + size / 2);
935 ctx.lineTo(x - size / 2, y);
936 ctx.fill();
937 ctx.stroke();
938 break;
939 case "triangle-right":
940 ctx.beginPath();
941 ctx.moveTo(x + size / 2, y);
942 ctx.lineTo(x - size / 2, y - size / 2);
943 ctx.lineTo(x - size / 2, y + size / 2);
944 ctx.lineTo(x + size / 2, y);
945 ctx.fill();
946 ctx.stroke();
947 break;
948 case "cross":
949 ctx.beginPath();
950 ctx.moveTo(x - size / 2, y - size / 2);
951 ctx.lineTo(x + size / 2, y + size / 2);
952 ctx.moveTo(x - size / 2, y + size / 2);
953 ctx.lineTo(x + size / 2, y - size / 2);
954 ctx.stroke();
955 break;
956 case "plus":
957 ctx.beginPath();
958 ctx.moveTo(x - size / 2, y);
959 ctx.lineTo(x + size / 2, y);
960 ctx.moveTo(x, y + size / 2);
961 ctx.lineTo(x, y - size / 2);
962 ctx.stroke();
963 break;
964 }
965}
966
967MPlotGraph.prototype.draw = function () {
968
969 //profile();
970
971 if (!this.canvas)
972 return;
973
974 let ctx = this.canvas.getContext("2d");
975
976 this.width = this.canvas.width;
977 this.height = this.canvas.height;
978
979 ctx.fillStyle = this.param.color.background;
980 ctx.fillRect(0, 0, this.width, this.height);
981
982 if (this.error !== null) {
983 ctx.lineWidth = 1;
984 ctx.font = "14px sans-serif";
985 ctx.strokeStyle = "#808080";
986 ctx.fillStyle = "#808080";
987 ctx.textAlign = "center";
988 ctx.textBaseline = "middle";
989 ctx.fillText(this.error, this.width / 2, this.height / 2);
990 return;
991 }
992
993 if (this.param.plot[0].xData === undefined) {
994 ctx.lineWidth = 1;
995 ctx.font = "14px sans-serif";
996 ctx.strokeStyle = "#808080";
997 ctx.fillStyle = "#808080";
998 ctx.textAlign = "center";
999 ctx.textBaseline = "middle";
1000 ctx.fillText("No data-odb-path present and no setData() called", this.width / 2, this.height / 2);
1001 return;
1002 }
1003
1004 if (this.height === undefined || this.width === undefined)
1005 return;
1006 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1007 return;
1008
1009 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1010
1011 let axisLabelWidth = this.drawYAxis(ctx, 50, this.height - 25, this.height - 35,
1012 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.param.yAxis.log, false);
1013
1014 if (axisLabelWidth === undefined)
1015 return;
1016
1017 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1018 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1019 else
1020 this.x1 = axisLabelWidth + 15;
1021
1022 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "")
1023 this.y1 = this.height - this.param.xAxis.textSize - 1.5*this.param.xAxis.title.textSize - 10;
1024 else
1025 this.y1 = this.height - this.param.xAxis.textSize - 10;
1026
1027 this.x2 = this.param.showMenuButtons ? this.width - 30 : this.width - 2;
1028 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "")
1029 this.x2 -= 1.0*this.param.zAxis.title.textSize;
1030
1031 this.y2 = 6;
1032
1033 if (this.param.showMenuButtons === false)
1034 this.x2 = this.width - 2;
1035
1036 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1037 if (this.zMin === undefined || this.zMax === undefined) {
1038 this.zMin = 0;
1039 this.zMax = 1;
1040 }
1041 if (this.zMin === this.zMax) {
1042 this.zMin -= 0.5;
1043 this.zMax += 0.5;
1044 }
1045
1046 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1047 axisLabelWidth = this.drawYAxis(ctx, this.x2 + 30, this.y1, this.y1 - this.y2,
1048 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, false);
1049 if (axisLabelWidth === undefined)
1050 return;
1051
1052 if (this.param.zAxis.show) {
1053 let w = 5; // left gap
1054 w += 10; // color bar
1055 w += 12; // tick width
1056 w += 5;
1057
1058 this.x2 -= axisLabelWidth + w;
1059 this.param.zAxis.width = axisLabelWidth + w;
1060 }
1061 }
1062
1063 // title
1064 if (this.param.title.text !== "") {
1065 ctx.strokeStyle = this.param.color.axis;
1066 ctx.fillStyle = "#F0F0F0";
1067 ctx.font = this.param.title.textSize + "px sans-serif";
1068 let h = this.param.title.textSize * 1.2;
1069 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, h);
1070 ctx.fillRect(this.x1, 6, this.x2 - this.x1, h);
1071 ctx.textAlign = "center";
1072 ctx.textBaseline = "middle";
1073 ctx.fillStyle = this.param.title.color;
1074 ctx.fillText(this.param.title.text, (this.x2 + this.x1) / 2, 6 + h/2);
1075 this.y2 = 6 + h;
1076 }
1077
1078 // draw axis
1079 ctx.strokeStyle = this.param.color.axis;
1080 ctx.drawLine(this.x1, this.y2, this.x2, this.y2);
1081 ctx.drawLine(this.x2, this.y2, this.x2, this.y1);
1082
1083 if (this.param.yAxis.log && this.yMin < 1E-20)
1084 this.yMin = 1E-20;
1085 if (this.param.yAxis.log && this.yMax < 1E-18)
1086 this.yMax = 1E-18;
1087
1088 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
1089 ctx.save();
1090 ctx.fillStyle = this.param.title.color;
1091 let s = this.param.xAxis.title.textSize;
1092 ctx.font = s + "px sans-serif";
1093 ctx.textAlign = "center";
1094 ctx.textBaseline = "top";
1095 ctx.fillText(this.param.xAxis.title.text, (this.x1 + this.x2)/2,
1096 this.y1 + this.param.xAxis.textSize + 10 + this.param.xAxis.title.textSize / 4);
1097 ctx.restore();
1098 }
1099
1100 ctx.font = this.param.xAxis.textSize + "px sans-serif";
1101 let grid = this.param.xAxis.grid ? this.y2 - this.y1 : 0;
1102 this.drawXAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1103 4, 7, 10, 10, grid, this.xMin, this.xMax, this.param.xAxis.log);
1104
1105 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1106 ctx.save();
1107 ctx.fillStyle = this.param.title.color;
1108 let s = this.param.yAxis.title.textSize;
1109 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1110 ctx.rotate(-Math.PI / 2);
1111 ctx.font = s + "px sans-serif";
1112 ctx.textAlign = "center";
1113 ctx.textBaseline = "top";
1114 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1115 ctx.restore();
1116 }
1117
1118 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
1119 ctx.save();
1120 ctx.fillStyle = this.param.title.color;
1121 let s = this.param.zAxis.title.textSize;
1122 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1123 ctx.rotate(-Math.PI / 2);
1124 ctx.font = s + "px sans-serif";
1125 ctx.textAlign = "center";
1126 ctx.textBaseline = "middle";
1127 ctx.fillText(this.param.zAxis.title.text, 0, this.x2 + this.param.zAxis.width);
1128 ctx.restore();
1129 }
1130
1131 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1132 grid = this.param.yAxis.grid ? this.x2 - this.x1 : 0;
1133 this.drawYAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
1134 -4, -7, -10, -12, grid, this.yMin, this.yMax, this.param.yAxis.log, true);
1135
1136 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1137 ctx.save();
1138 let s = this.param.yAxis.title.textSize;
1139 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1140 ctx.rotate(-Math.PI / 2);
1141 ctx.font = s + "px sans-serif";
1142 ctx.textAlign = "center";
1143 ctx.textBaseline = "top";
1144 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1145 ctx.restore();
1146 }
1147
1148 ctx.save();
1149 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
1150 ctx.clip();
1151
1152 // draw graphs
1153 let noData = true;
1154 for (const p of this.param.plot) {
1155 if (p.xData === undefined || p.xData === null)
1156 continue;
1157
1158 if (p.xData.length > 0)
1159 noData = false;
1160
1161 ctx.globalAlpha = p.alpha;
1162
1163 if (p.type === "scatter") {
1164 // draw lines
1165 if (p.line && p.line.draw ||
1166 p.line && p.line.fill) {
1167
1168 if (typeof p.line.color === "string")
1169 ctx.fillStyle = p.line.color;
1170 else if (typeof p.line.color === "number")
1171 ctx.fillStyle = this.param.color.data[p.line.color];
1172 ctx.strokeStyle = ctx.fillStyle;
1173
1174 // shaded area
1175 if (p.line.fill) {
1176 ctx.globalAlpha = 0.1;
1177 ctx.beginPath();
1178 ctx.moveTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1179 for (let i = 0; i < p.xData.length; i++) {
1180 let x = this.xToScreen(p.xData[i]);
1181 let y = this.yToScreen(p.yData[i]);
1182 ctx.lineTo(x, y);
1183 }
1184 ctx.lineTo(this.xToScreen(p.xData[p.xData.length - 1]), this.yToScreen(0));
1185 ctx.lineTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1186 ctx.fill();
1187 ctx.globalAlpha = 1;
1188 }
1189
1190 // draw line
1191 if (p.line.draw) {
1192 ctx.lineWidth = p.line.width;
1193 ctx.beginPath();
1194 for (let i = 0; i < p.xData.length; i++) {
1195 let x = this.xToScreen(p.xData[i]);
1196 let y = this.yToScreen(p.yData[i]);
1197 if (i === 0)
1198 ctx.moveTo(x, y);
1199 else
1200 ctx.lineTo(x, y);
1201 }
1202 ctx.stroke();
1203 }
1204 }
1205
1206 // draw markers
1207 if (p.marker && p.marker.draw) {
1208 for (let i = 0; i < p.xData.length; i++) {
1209
1210 let x = this.xToScreen(p.xData[i]);
1211 let y = this.yToScreen(p.yData[i]);
1212
1213 this.drawMarker(ctx, p, x, y);
1214 }
1215 }
1216 }
1217
1218 else if (p.type === "histogram") {
1219 let x, y;
1220 let dx = (p.xMax - p.xMin) / p.xData.length;
1221 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1222
1223 if (p.length < 100)
1224 ctx.lineWidth = 2;
1225 else
1226 ctx.lineWidth = 1;
1227
1228 if (typeof p.line.color === "string")
1229 ctx.fillStyle = p.line.color;
1230 else if (typeof p.line.color === "number")
1231 ctx.fillStyle = this.param.color.data[p.line.color];
1232 ctx.strokeStyle = ctx.fillStyle;
1233
1234 ctx.beginPath();
1235 ctx.moveTo(this.xToScreen(p.xData[0])-dxs/2, this.yToScreen(0));
1236 for (let i = 0; i < p.xData.length; i++) {
1237 x = this.xToScreen(p.xData[i]);
1238 y = this.yToScreen(p.yData[i]);
1239 ctx.lineTo(x-dxs/2, y);
1240 ctx.lineTo(x+dxs/2, y);
1241 }
1242 ctx.lineTo(x+dxs/2, this.yToScreen(0));
1243 ctx.globalAlpha = 0.2;
1244 ctx.fill();
1245 ctx.globalAlpha = 1;
1246 ctx.stroke();
1247 }
1248
1249 else if (p.type === "colormap") {
1250 let dx = (p.xMax - p.xMin) / this.nx;
1251 let dy = (p.yMax - p.yMin) / this.ny;
1252
1253 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1254 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
1255
1256 for (let i=0 ; i<p.ny ; i++) {
1257 for (let j=0 ; j<p.nx ; j++) {
1258 let x = this.xToScreen(j * dx + p.xMin);
1259 let y = this.yToScreen(i * dy + p.yMin);
1260 let zval = this.param.plot[0].zData[j+i*p.nx];
1261 if (isNaN(zval)) {
1262 ctx.fillStyle = 'hsl(255, 0%, 50%)';
1263 } else {
1264 let v;
1265 if (this.param.zAxis.log) {
1266 if (zval <= 0)
1267 v = 0;
1268 else
1269 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
1270 } else
1271 v = (zval - this.zMin) / (this.zMax - this.zMin);
1272
1273 // limit v to 0...1
1274 if (v < 0)
1275 v = 0;
1276 if (v > 1)
1277 v = 1;
1278
1279 if (zval < 0.5 && this.param.plot[0].bgcolor)
1280 ctx.fillStyle = this.param.plot[0].bgcolor;
1281 else
1282 ctx.fillStyle = 'hsl(' + Math.floor((1 - v) * 240) + ', 100%, 50%)';
1283 }
1284 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
1285 }
1286 }
1287 //profile("plot");
1288 }
1289 }
1290
1291 ctx.restore(); // remove clipping
1292
1293 // plot color scale
1294 if (this.param.plot[0].type === "colormap") {
1295 if (this.param.plot[0].showZScale) {
1296
1297 for (let i=0 ; i<100 ; i++) {
1298 let v = i / 100;
1299 ctx.fillStyle = 'hsl(' +
1300 Math.floor(v * 240) + ', 100%, 50%)';
1301 ctx.fillRect(this.x2 + 5, this.y2 + i/100*(this.y1 - this.y2),
1302 10, (this.y1 - this.y2) / 100 + 1);
1303 }
1304
1305 ctx.lineWidth = 1;
1306 ctx.strokeStyle = this.param.color.axis;
1307 ctx.beginPath();
1308 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
1309 ctx.stroke();
1310
1311 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1312 ctx.strokeStyle = this.param.color.axis;
1313
1314 this.drawYAxis(ctx, this.x2 + 15, this.y1, this.y1 - this.y2,
1315 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, true);
1316 }
1317 }
1318
1319 // plot legend
1320 let nLabel = 0;
1321 for (const p of this.param.plot)
1322 if (p.label && p.label !== "")
1323 nLabel++;
1324
1325 if (this.param.legend && this.param.legend.show && nLabel > 0) {
1326 ctx.font = this.param.legend.textSize + "px sans-serif";
1327
1328 let mw = 0;
1329 for (const p of this.param.plot) {
1330 if (ctx.measureText(p.label).width > mw) {
1331 mw = ctx.measureText(p.label).width;
1332 }
1333 }
1334 let w = 50 + mw + 5;
1335 let h = this.param.legend.textSize * 1.5;
1336
1337 ctx.fillStyle = this.param.legend.backgroundColor;
1338 ctx.strokeStyle = this.param.legend.color;
1339 ctx.fillRect(this.x1 + 10, this.y2 + 10, w, h * this.param.plot.length);
1340 ctx.strokeRect(this.x1 + 10, this.y2 + 10, w, h * this.param.plot.length);
1341
1342 for (const [pi,p] of this.param.plot.entries()) {
1343 if (p.line) {
1344 ctx.beginPath();
1345 ctx.strokeStyle = this.param.color.data[p.line.color];
1346 ctx.lineWidth = p.line.width;
1347 ctx.beginPath();
1348 ctx.moveTo(this.x1 + 15, this.y2 + 10 + pi*h + h/2);
1349 ctx.lineTo(this.x1 + 45, this.y2 + 10 + pi*h + h/2);
1350 ctx.stroke();
1351 }
1352 if (p.marker) {
1353 this.drawMarker(ctx, p, this.x1 + 30, this.y2 + 10 + pi*h + h/2);
1354 }
1355 ctx.textAlign = "left";
1356 ctx.textBaseline = "middle";
1357 ctx.fillStyle = this.param.color.axis;
1358 ctx.fillText(p.label, this.x1 + 50, this.y2 + 10 + pi*h + h/2);
1359 }
1360 }
1361
1362 // "empty window" notice
1363 if (noData) {
1364 ctx.font = "16px sans-serif";
1365 let str = "No data available";
1366 ctx.strokeStyle = "#404040";
1367 ctx.fillStyle = "#F0F0F0";
1368 let w = ctx.measureText(str).width + 10;
1369 let h = 16 + 10;
1370 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1371 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1372 ctx.fillStyle = "#404040";
1373 ctx.textAlign = "center";
1374 ctx.textBaseline = "middle";
1375 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
1376 ctx.font = "14px sans-serif";
1377 }
1378
1379 // buttons
1380 if (this.param.showMenuButtons) {
1381 let y = 0;
1382 let buttonSize = 20;
1383 this.button.forEach(b => {
1384 b.x1 = this.width - buttonSize - 6;
1385 b.y1 = 6 + y * (buttonSize + 4);
1386 b.width = buttonSize + 4;
1387 b.height = buttonSize + 4;
1388 b.enabled = true;
1389
1390 ctx.fillStyle = "#F0F0F0";
1391 ctx.strokeStyle = "#808080";
1392 ctx.fillRect(b.x1, b.y1, b.width, b.height);
1393 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
1394 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
1395
1396 y++;
1397 });
1398 }
1399
1400 // axis zoom
1401 if (this.zoom.x.active) {
1402 ctx.fillStyle = "#808080";
1403 ctx.globalAlpha = 0.2;
1404 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
1405 ctx.globalAlpha = 1;
1406 ctx.strokeStyle = "#808080";
1407 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
1408 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
1409 }
1410 if (this.zoom.y.active) {
1411 ctx.fillStyle = "#808080";
1412 ctx.globalAlpha = 0.2;
1413 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
1414 ctx.globalAlpha = 1;
1415 ctx.strokeStyle = "#808080";
1416 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
1417 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
1418 }
1419
1420 // marker
1421 if (this.marker.active) {
1422
1423 // round marker
1424 if (this.param.plot[0].type !== "colormap") {
1425 ctx.beginPath();
1426 ctx.globalAlpha = 0.1;
1427 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
1428 ctx.fillStyle = "#000000";
1429 ctx.fill();
1430 ctx.globalAlpha = 1;
1431
1432 ctx.beginPath();
1433 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
1434 ctx.fillStyle = "#000000";
1435 ctx.fill();
1436 }
1437
1438 ctx.strokeStyle = "#A0A0A0";
1439 ctx.drawLine(this.marker.sx, this.y1, this.marker.sx, this.y2);
1440 ctx.drawLine(this.x1, this.marker.sy, this.x2, this.marker.sy);
1441
1442 // text label
1443 ctx.font = "12px sans-serif";
1444 ctx.textAlign = "left";
1445 let s;
1446 if (this.parentDiv.dataset.tooltip) {
1447 let f = this.parentDiv.dataset.tooltip;
1448 if (f.indexOf('(') !== -1) // strip any '('
1449 f = f.substring(0, f.indexOf('('));
1450
1451 s = eval(f + "(this)");
1452 } else {
1453 s = this.marker.x.toPrecision(6).stripZeros() + " / " +
1454 this.marker.y.toPrecision(6).stripZeros();
1455 if (this.param.plot[0].type === "colormap")
1456 s += ": " + (this.marker.z === null ? "null" : this.marker.z.toPrecision(6).stripZeros());
1457 }
1458 let w = ctx.measureText(s).width + 6;
1459 let h = ctx.measureText("M").width * 1.2 + 6;
1460 let x = this.marker.mx + 10;
1461 let y = this.marker.my - 20;
1462
1463 // move marker inside if outside plotting area
1464 if (x + w >= this.x2)
1465 x = this.marker.sx - 10 - w;
1466
1467 ctx.strokeStyle = "#808080";
1468 ctx.fillStyle = "#F0F0F0";
1469 ctx.textBaseline = "middle";
1470 ctx.fillRect(x, y, w, h);
1471 ctx.strokeRect(x, y, w, h);
1472 ctx.fillStyle = "#404040";
1473 ctx.fillText(s, x + 3, y + h / 2);
1474 }
1475
1476 // call optional user overlay function
1477 if (this.param.overlay) {
1478
1479 // set default text
1480 ctx.textAlign = "left";
1481 ctx.textBaseline = "top";
1482 ctx.fillStyle = "black";
1483 ctx.strokeStyle = "black";
1484 ctx.font = "12px sans-serif";
1485
1486 eval(this.param.overlay + "(this, ctx)");
1487 }
1488
1489 //profile("end");
1490}
1491
1492LN10 = 2.302585094;
1493LOG2 = 0.301029996;
1494LOG5 = 0.698970005;
1495
1496MPlotGraph.prototype.drawXAxis = function (ctx, x1, y1, width, minor, major,
1497 text, label, grid, xmin, xmax, logaxis) {
1498 var dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1499 var tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1500 var base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1501
1502 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
1503 return;
1504
1505 if (xmax <= xmin || width <= 0)
1506 return;
1507
1508 ctx.textAlign = "center";
1509 ctx.textBaseline = "top";
1510
1511 if (logaxis) {
1512
1513 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1514 if (isNaN(dx) || dx === 0) {
1515 xmin = 1E-20;
1516 dx = 1E-20;
1517 }
1518 label_dx = dx;
1519 major_dx = dx * 10;
1520 n_sig1 = 4;
1521
1522 } else { // linear axis ----
1523
1524 // use 10 as min tick distance
1525 dx = (xmax - xmin) / (width / 10);
1526
1527 int_dx = Math.floor(Math.log(dx) / LN10);
1528 frac_dx = Math.log(dx) / LN10 - int_dx;
1529
1530 if (frac_dx < 0) {
1531 frac_dx += 1;
1532 int_dx -= 1;
1533 }
1534
1535 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
1536 major_base = label_base = tick_base + 1;
1537
1538 // rounding up of dx, label_dx
1539 dx = Math.pow(10, int_dx) * base[tick_base];
1540 major_dx = Math.pow(10, int_dx) * base[major_base];
1541 label_dx = major_dx;
1542
1543 do {
1544 // number of significant digits
1545 if (xmin === 0)
1546 n_sig1 = 0;
1547 else
1548 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
1549
1550 if (xmax === 0)
1551 n_sig2 = 0;
1552 else
1553 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
1554
1555 n_sig1 = Math.max(n_sig1, n_sig2);
1556
1557 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
1558 if (Math.abs(xmin) < 100000)
1559 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / LN10) + 1);
1560 if (Math.abs(xmax) < 100000)
1561 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / LN10) + 1);
1562
1563 // determination of maximal width of labels
1564 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
1565 let ext = ctx.measureText(str);
1566 maxwidth = ext.width;
1567
1568 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
1569 ext = ctx.measureText(str);
1570 maxwidth = Math.max(maxwidth, ext.width);
1571 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
1572 maxwidth = Math.max(maxwidth, ext.width);
1573
1574 // increasing label_dx, if labels would overlap
1575 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1576 label_base++;
1577 label_dx = Math.pow(10, int_dx) * base[label_base];
1578 if (label_base % 3 === 2 && major_base % 3 === 1) {
1579 major_base++;
1580 major_dx = Math.pow(10, int_dx) * base[major_base];
1581 }
1582 } else
1583 break;
1584
1585 } while (true);
1586 }
1587
1588 x_act = Math.floor(xmin / dx) * dx;
1589
1590 ctx.strokeStyle = this.param.color.axis;
1591 ctx.drawLine(x1, y1, x1 + width, y1);
1592
1593 do {
1594 if (logaxis)
1595 x_screen = (Math.log(x_act) - Math.log(xmin)) /
1596 (Math.log(xmax) - Math.log(xmin)) * width + x1;
1597 else
1598 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1599 xs = Math.floor(x_screen + 0.5);
1600
1601 if (x_screen > x1 + width + 0.001)
1602 break;
1603
1604 if (x_screen >= x1) {
1605 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
1606 dx / major_dx / 10.0) {
1607
1608 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1609 dx / label_dx / 10.0) {
1610 // label tick mark
1611 ctx.strokeStyle = this.param.color.axis;
1612 ctx.drawLine(xs, y1, xs, y1 + text);
1613
1614 // grid line
1615 if (grid !== 0 && xs > x1 && xs < x1 + width) {
1616 ctx.strokeStyle = this.param.color.grid;
1617 ctx.drawLine(xs, y1, xs, y1 + grid);
1618 }
1619
1620 // label
1621 if (label !== 0) {
1622 str = x_act.toPrecision(n_sig1).stripZeros();
1623 ext = ctx.measureText(str);
1624 if (xs - ext.width / 2 > x1 &&
1625 xs + ext.width / 2 < x1 + width) {
1626 ctx.strokeStyle = this.param.color.label;
1627 ctx.fillStyle = this.param.color.label;
1628 ctx.fillText(str, xs, y1 + label);
1629 }
1630 last_label_x = xs + ext.width / 2;
1631 }
1632 } else {
1633 // major tick mark
1634 ctx.strokeStyle = this.param.color.axis;
1635 ctx.drawLine(xs, y1, xs, y1 + major);
1636
1637 // grid line
1638 if (grid !== 0 && xs > x1 && xs < x1 + width) {
1639 ctx.strokeStyle = this.param.color.grid;
1640 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1641 }
1642 }
1643
1644 if (logaxis) {
1645 dx *= 10;
1646 major_dx *= 10;
1647 label_dx *= 10;
1648 }
1649
1650 } else {
1651 // minor tick mark
1652 ctx.strokeStyle = this.param.color.axis;
1653 ctx.drawLine(xs, y1, xs, y1 + minor);
1654 }
1655
1656 if (logaxis) {
1657 // for log axis, also put grid lines on minor tick marks
1658 if (grid !== 0 && xs > x1 && xs < x1 + width) {
1659 ctx.strokeStyle = this.param.color.grid;
1660 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1661 }
1662
1663 // for log axis, also put labels on minor tick marks
1664 if (label !== 0) {
1665 let str;
1666 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
1667 str = x_act.toExponential(n_sig1).stripZeros();
1668 else
1669 str = x_act.toPrecision(n_sig1).stripZeros();
1670 ext = ctx.measureText(str);
1671 if (xs - ext.width / 2 > x1 &&
1672 xs + ext.width / 2 < x1 + width &&
1673 xs - ext.width / 2 > last_label_x + 5) {
1674 ctx.strokeStyle = this.param.color.label;
1675 ctx.fillStyle = this.param.color.label;
1676 ctx.fillText(str, xs, y1 + label);
1677 }
1678
1679 last_label_x = xs + ext.width / 2;
1680 }
1681 }
1682 }
1683
1684 x_act += dx;
1685
1686 /* suppress 1.23E-17 ... */
1687 if (Math.abs(x_act) < dx / 100)
1688 x_act = 0;
1689
1690 } while (1);
1691}
1692
1693
1694MPlotGraph.prototype.drawYAxis = function (ctx, x1, y1, height, minor, major,
1695 text, label, grid, ymin, ymax, logaxis, draw) {
1696 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
1697 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
1698 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1699
1700 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
1701 return;
1702
1703 if (ymax <= ymin || height <= 0)
1704 return;
1705
1706 if (label < 0)
1707 ctx.textAlign = "right";
1708 else
1709 ctx.textAlign = "left";
1710 ctx.textBaseline = "middle";
1711 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1712
1713 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
1714 dy = Number.MAX_VALUE / 10;
1715 label_dy = dy;
1716 major_dy = dy;
1717 n_sig1 = 1;
1718 } else if (logaxis) {
1719 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1720 if (isNaN(dy) || dy === 0) {
1721 ymin = 1E-20;
1722 dy = 1E-20;
1723 }
1724 label_dy = dy;
1725 major_dy = dy * 10;
1726 n_sig1 = 4;
1727 } else {
1728 // use 6 as min tick distance
1729 dy = (ymax - ymin) / (height / 6);
1730
1731 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1732 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
1733
1734 if (frac_dy < 0) {
1735 frac_dy += 1;
1736 int_dy -= 1;
1737 }
1738
1739 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
1740 major_base = label_base = tick_base + 1;
1741
1742 // rounding up of dy, label_dy
1743 dy = Math.pow(10, int_dy) * base[tick_base];
1744 major_dy = Math.pow(10, int_dy) * base[major_base];
1745 label_dy = major_dy;
1746
1747 // number of significant digits
1748 if (ymin === 0)
1749 n_sig1 = 1;
1750 else
1751 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
1752 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1753
1754 if (ymax === 0)
1755 n_sig2 = 1;
1756 else
1757 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
1758 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1759
1760 n_sig1 = Math.max(n_sig1, n_sig2);
1761 n_sig1 = Math.max(1, n_sig1);
1762
1763 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
1764 if (Math.abs(ymin) < 100000)
1765 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
1766 Math.log(10) + 0.001) + 1);
1767 if (Math.abs(ymax) < 100000)
1768 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
1769 Math.log(10) + 0.001) + 1);
1770
1771 // increase label_dy if labels would overlap
1772 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1773 label_base++;
1774 label_dy = Math.pow(10, int_dy) * base[label_base];
1775 if (label_base % 3 === 2 && major_base % 3 === 1) {
1776 major_base++;
1777 major_dy = Math.pow(10, int_dy) * base[major_base];
1778 }
1779 }
1780 }
1781
1782 y_act = Math.floor(ymin / dy) * dy;
1783
1784 let last_label_y = y1;
1785 let maxwidth = 0;
1786
1787 if (draw) {
1788 ctx.strokeStyle = this.param.color.axis;
1789 ctx.drawLine(x1, y1, x1, y1 - height);
1790 }
1791
1792 do {
1793 if (logaxis)
1794 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
1795 (Math.log(ymax) - Math.log(ymin)) * height;
1796 else if (!(isFinite(ymax - ymin)))
1797 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
1798 else
1799 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1800 ys = Math.round(y_screen);
1801
1802 if (y_screen < y1 - height - 0.001 || isNaN(ys))
1803 break;
1804
1805 if (y_screen <= y1 + 0.001) {
1806 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
1807 dy / major_dy / 10.0) {
1808
1809 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
1810 dy / label_dy / 10.0) {
1811 // label tick mark
1812 if (draw) {
1813 ctx.strokeStyle = this.param.color.axis;
1814 ctx.drawLine(x1, ys, x1 + text, ys);
1815 }
1816
1817 // grid line
1818 if (grid !== 0 && ys < y1 && ys > y1 - height)
1819 if (draw) {
1820 ctx.strokeStyle = this.param.color.grid;
1821 ctx.drawLine(x1, ys, x1 + grid, ys);
1822 }
1823
1824 // label
1825 if (label !== 0) {
1826 let str;
1827 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
1828 str = y_act.toExponential(n_sig1).stripZeros();
1829 else
1830 str = y_act.toPrecision(n_sig1).stripZeros();
1831 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
1832 if (draw) {
1833 ctx.strokeStyle = this.param.color.label;
1834 ctx.fillStyle = this.param.color.label;
1835 ctx.fillText(str, x1 + label, ys);
1836 }
1837 last_label_y = ys - textHeight / 2;
1838 }
1839 } else {
1840 // major tick mark
1841 if (draw) {
1842 ctx.strokeStyle = this.param.color.axis;
1843 ctx.drawLine(x1, ys, x1 + major, ys);
1844 }
1845
1846 // grid line
1847 if (grid !== 0 && ys < y1 && ys > y1 - height)
1848 if (draw) {
1849 ctx.strokeStyle = this.param.color.grid;
1850 ctx.drawLine(x1, ys, x1 + grid, ys);
1851 }
1852 }
1853
1854 if (logaxis) {
1855 dy *= 10;
1856 major_dy *= 10;
1857 label_dy *= 10;
1858 }
1859
1860 } else {
1861 // minor tick mark
1862 if (draw) {
1863 ctx.strokeStyle = this.param.color.axis;
1864 ctx.drawLine(x1, ys, x1 + minor, ys);
1865 }
1866 }
1867
1868 if (logaxis) {
1869
1870 // for log axis, also put grid lines on minor tick marks
1871 if (grid !== 0 && ys < y1 && ys > y1 - height) {
1872 if (draw) {
1873 ctx.strokeStyle = this.param.color.grid;
1874 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
1875 }
1876 }
1877
1878 // for log axis, also put labels on minor tick marks
1879 if (label !== 0) {
1880 let str;
1881 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
1882 str = y_act.toExponential(n_sig1).stripZeros();
1883 else
1884 str = y_act.toPrecision(n_sig1).stripZeros();
1885 if (ys - textHeight / 2 > y1 - height &&
1886 ys + textHeight / 2 < y1 &&
1887 ys + textHeight < last_label_y + 2) {
1888 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
1889 if (draw) {
1890 ctx.strokeStyle = this.param.color.label;
1891 ctx.fillStyle = this.param.color.label;
1892 ctx.fillText(str, x1 + label, ys);
1893 }
1894 }
1895
1896 last_label_y = ys;
1897 }
1898 }
1899 }
1900
1901 y_act += dy;
1902
1903 // suppress 1.23E-17 ...
1904 if (Math.abs(y_act) < dy / 100)
1905 y_act = 0;
1906
1907 } while (1);
1908
1909 return maxwidth;
1910};
1911
1912MPlotGraph.prototype.download = function (mode) {
1913
1914 let d = new Date();
1915 let filename = this.param.title.text + "-" +
1916 d.getFullYear() +
1917 ("0" + (d.getUTCMonth() + 1)).slice(-2) +
1918 ("0" + d.getUTCDate()).slice(-2) + "-" +
1919 ("0" + d.getUTCHours()).slice(-2) +
1920 ("0" + d.getUTCMinutes()).slice(-2) +
1921 ("0" + d.getUTCSeconds()).slice(-2);
1922
1923 // use trick from FileSaver.js
1924 let a = document.getElementById('downloadHook');
1925 if (a === null) {
1926 a = document.createElement("a");
1927 a.style.display = "none";
1928 a.id = "downloadHook";
1929 document.body.appendChild(a);
1930 }
1931
1932 if (mode === "CSV") {
1933 filename += ".csv";
1934
1935 let data = "";
1936
1937 // title
1938 this.param.plot.forEach(p => {
1939 if (p.type === "scatter" || p.type === "histogram") {
1940 data += "X,";
1941 if (p.label === "")
1942 data += "Y";
1943 else
1944 data += p.label;
1945 data += '\n';
1946
1947 // data
1948 for (let i = 0; i < p.xData.length; i++) {
1949 data += p.xData[i] + ",";
1950 data += p.yData[i] + "\n";
1951 }
1952 data += '\n';
1953 }
1954
1955 if (p.type === "colormap") {
1956 data += "X \ Y,";
1957
1958 // X-header
1959 for (let i = 0; i < p.nx; i++)
1960 data += p.xData[i] + ",";
1961 data += '\n';
1962
1963 for (let j = 0; j < p.ny; j++) {
1964 data += p.yData[j] + ",";
1965 for (let i = 0; i < p.nx; i++)
1966 data += p.zData[i + j * p.nx] + ",";
1967 data += '\n';
1968 }
1969 }
1970 });
1971
1972 let blob = new Blob([data], {type: "text/csv"});
1973 let url = window.URL.createObjectURL(blob);
1974
1975 a.href = url;
1976 a.download = filename;
1977 a.click();
1978 window.URL.revokeObjectURL(url);
1979 dlgAlert("Data downloaded to '" + filename + "'");
1980
1981 } else if (mode === "PNG") {
1982 filename += ".png";
1983
1984 let smb = this.param.showMenuButtons;
1985 this.param.showMenuButtons = false;
1986 this.draw();
1987
1988 let h = this;
1989 this.canvas.toBlob(function (blob) {
1990 let url = window.URL.createObjectURL(blob);
1991
1992 a.href = url;
1993 a.download = filename;
1994 a.click();
1995 window.URL.revokeObjectURL(url);
1996 dlgAlert("Image downloaded to '" + filename + "'");
1997
1998 h.param.showMenuButtons = smb;
1999 h.redraw();
2000
2001 }, 'image/png');
2002 }
2003
2004};
2005
2006MPlotGraph.prototype.drawTextBox = function (ctx, text, x, y) {
2007 let line = text.split("\n");
2008
2009 let mw = 0;
2010 for (const p of line)
2011 if (ctx.measureText(p).width > mw)
2012 mw = ctx.measureText(p).width;
2013 let w = 5 + mw + 5;
2014 let h = parseInt(ctx.font) * 1.5;
2015
2016 let c = ctx.fillStyle;
2017 ctx.fillStyle = "white";
2018 ctx.fillRect(x, y, w, h * line.length);
2019 ctx.fillStyle = c;
2020 ctx.strokeRect(x, y, w, h * line.length);
2021
2022 for (let i=0 ; i<line.length ; i++)
2023 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
2024}
2025
2026MPlotGraph.prototype.mouseEvent = function (e) {
2027
2028 // execute callback if registered
2029 if (this.param.event) {
2030
2031 if (this.param.plot[0].type === "colormap") {
2032 // pass plot column/row to callback
2033 let x = this.screenToX(e.offsetX);
2034 let y = this.screenToY(e.offsetY);
2035 let xMin = this.param.plot[0].xMin;
2036 let xMax = this.param.plot[0].xMax;
2037 let yMin = this.param.plot[0].yMin;
2038 let yMax = this.param.plot[0].yMax;
2039 let dx = (xMax - xMin) / this.nx;
2040 let dy = (yMax - yMin) / this.ny;
2041 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
2042 x > xMin && x < xMax && y > yMin && y < yMax) {
2043 let ix = Math.floor((x - xMin) / dx);
2044 let iy = Math.floor((y - yMin) / dy);
2045
2046 let flag = eval(this.param.event + "(e, this, ix, iy)");
2047 if (flag)
2048 return;
2049 }
2050 } else {
2051
2052 // call all other plots only with event and object
2053 let flag = eval(this.param.event + "(e, this)");
2054 if (flag)
2055 return;
2056
2057 }
2058 }
2059
2060 // fix buttons for IE
2061 if (!e.which && e.button) {
2062 if ((e.button & 1) > 0) e.which = 1; // Left
2063 else if ((e.button & 4) > 0) e.which = 2; // Middle
2064 else if ((e.button & 2) > 0) e.which = 3; // Right
2065 }
2066
2067 let cursor = "default";
2068 let title = "";
2069 let cancel = false;
2070
2071 // cancel dragging in case we did not catch the mouseup event
2072 if (e.type === "mousemove" && e.buttons === 0 &&
2073 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
2074 cancel = true;
2075
2076 if (e.type === "mousedown") {
2077
2078 this.downloadSelector.style.display = "none";
2079
2080 // check for buttons
2081 this.button.forEach(b => {
2082 if (e.offsetX > b.x1 && e.offsetX < b.x1 + b.width &&
2083 e.offsetY > b.y1 && e.offsetY < b.y1 + b.width &&
2084 b.enabled) {
2085 b.click(this);
2086 }
2087 });
2088
2089 // check for dragging
2090 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2091 e.offsetY > this.y2 && e.offsetY < this.y1) {
2092 this.drag.active = true;
2093 this.marker.active = false;
2094 this.drag.sxStart = e.offsetX;
2095 this.drag.syStart = e.offsetY;
2096 this.drag.xStart = this.screenToX(e.offsetX);
2097 this.drag.yStart = this.screenToY(e.offsetY);
2098 this.drag.xMinStart = this.xMin;
2099 this.drag.xMaxStart = this.xMax;
2100 this.drag.yMinStart = this.yMin;
2101 this.drag.yMaxStart = this.yMax;
2102
2103 this.blockAutoScale = true;
2104 }
2105
2106 // check for axis dragging
2107 if (e.offsetX > this.x1 && e.offsetX < this.x2 && e.offsetY > this.y1) {
2108 this.zoom.x.active = true;
2109 this.zoom.x.x1 = e.offsetX;
2110 this.zoom.x.x2 = undefined;
2111 this.zoom.x.t1 = this.screenToX(e.offsetX);
2112 }
2113 if (e.offsetY < this.y1 && e.offsetY > this.y2 && e.offsetX < this.x1) {
2114 this.zoom.y.active = true;
2115 this.zoom.y.y1 = e.offsetY;
2116 this.zoom.y.y2 = undefined;
2117 this.zoom.y.v1 = this.screenToY(e.offsetY);
2118 }
2119
2120 } else if (cancel || e.type === "mouseup") {
2121
2122 if (this.drag.active)
2123 this.drag.active = false;
2124
2125 if (this.zoom.x.active) {
2126 if (this.zoom.x.x2 !== undefined &&
2127 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
2128 let x1 = this.zoom.x.t1;
2129 let x2 = this.screenToX(this.zoom.x.x2);
2130 if (x1 > x2)
2131 [x1, x2] = [x2, x1];
2132 this.xMin = x1;
2133 this.xMax = x2;
2134 }
2135 this.zoom.x.active = false;
2136 this.blockAutoScale = true;
2137 this.redraw();
2138 }
2139
2140 if (this.zoom.y.active) {
2141 if (this.zoom.y.y2 !== undefined &&
2142 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
2143 let y1 = this.zoom.y.v1;
2144 let y2 = this.screenToY(this.zoom.y.y2);
2145 if (y1 > y2)
2146 [y1, y2] = [y2, y1];
2147 this.yMin = y1;
2148 this.yMax = y2;
2149 }
2150 this.zoom.y.active = false;
2151 this.blockAutoScale = true;
2152 this.redraw();
2153 }
2154
2155 } else if (e.type === "mousemove") {
2156
2157 if (this.drag.active) {
2158
2159 // execute dragging
2160 cursor = "move";
2161
2162 if (this.param.xAxis.log) {
2163 let dx = e.offsetX - this.drag.sxStart;
2164
2165 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));
2166 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));
2167
2168 if (this.xMin <= 0)
2169 this.xMin = 1E-20;
2170 if (this.xMax <= 0)
2171 this.xMax = 1E-18;
2172 } else {
2173 let dx = (e.offsetX - this.drag.sxStart) / (this.x2 - this.x1) * (this.xMax - this.xMin);
2174 this.xMin = this.drag.xMinStart - dx;
2175 this.xMax = this.drag.xMaxStart - dx;
2176 }
2177
2178 if (this.param.yAxis.log) {
2179 let dy = e.offsetY - this.drag.syStart;
2180
2181 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));
2182 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));
2183
2184 if (this.yMin <= 0)
2185 this.yMin = 1E-20;
2186 if (this.yMax <= 0)
2187 this.yMax = 1E-18;
2188 } else {
2189 let dy = (this.drag.syStart - e.offsetY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
2190 this.yMin = this.drag.yMinStart - dy;
2191 this.yMax = this.drag.yMaxStart - dy;
2192 }
2193
2194 this.redraw();
2195
2196 } else {
2197
2198 // change cursor to pointer over buttons
2199 this.button.forEach(b => {
2200 if (e.offsetX > b.x1 && e.offsetY > b.y1 &&
2201 e.offsetX < b.x1 + b.width && e.offsetY < b.y1 + b.height) {
2202 cursor = "pointer";
2203 title = b.title;
2204 }
2205 });
2206
2207 // execute axis zoom
2208 if (this.zoom.x.active) {
2209 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, e.offsetX));
2210 this.zoom.x.t2 = this.screenToX(e.offsetX);
2211 this.redraw();
2212 }
2213 if (this.zoom.y.active) {
2214 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, e.offsetY));
2215 this.zoom.y.v2 = this.screenToY(e.offsetY);
2216 this.redraw();
2217 }
2218
2219 // check if cursor close to plot point
2220 if (this.param.plot[0].type === "scatter" || this.param.plot[0].type === "histogram") {
2221 let minDist = 10000;
2222 for (const [pi, p] of this.param.plot.entries()) {
2223 if (p.xData === undefined || p.xData === null)
2224 continue;
2225
2226 for (let i = 0; i < p.xData.length; i++) {
2227 let x = this.xToScreen(p.xData[i]);
2228 let y = this.yToScreen(p.yData[i]);
2229 let d = (e.offsetX - x) * (e.offsetX - x) +
2230 (e.offsetY - y) * (e.offsetY - y);
2231 if (d < minDist) {
2232 minDist = d;
2233 this.marker.x = p.xData[i];
2234 this.marker.y = p.yData[i];
2235 this.marker.sx = x;
2236 this.marker.sy = y;
2237 this.marker.mx = e.offsetX;
2238 this.marker.my = e.offsetY;
2239 this.marker.plotIndex = pi;
2240 this.marker.index = i;
2241 }
2242 }
2243 }
2244
2245 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
2246 }
2247
2248 if (this.param.plot[0].type === "colormap") {
2249 let x = this.screenToX(e.offsetX);
2250 let y = this.screenToY(e.offsetY);
2251 let xMin = this.param.plot[0].xMin;
2252 let xMax = this.param.plot[0].xMax;
2253 let yMin = this.param.plot[0].yMin;
2254 let yMax = this.param.plot[0].yMax;
2255 let dx = (xMax - xMin) / this.nx;
2256 let dy = (yMax - yMin) / this.ny;
2257 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
2258 x > xMin && x < xMax && y > yMin && y < yMax) {
2259 let i = Math.floor((x - xMin) / dx);
2260 let j = Math.floor((y - yMin) / dy);
2261
2262 this.marker.x = (i + 0.5) * dx + xMin;
2263 this.marker.y = (j + 0.5) * dy + yMin;
2264 this.marker.z = this.param.plot[0].zData[i + j * this.nx];
2265
2266 this.marker.sx = this.xToScreen(this.marker.x);
2267 this.marker.sy = this.yToScreen(this.marker.y);
2268 this.marker.mx = e.offsetX;
2269 this.marker.my = e.offsetY;
2270 this.marker.plotIndex = 0;
2271 this.marker.active = true;
2272 } else {
2273 this.marker.active = false;
2274 }
2275 }
2276
2277 this.draw();
2278 }
2279
2280 } else if (e.type === "wheel") {
2281
2282 let x = this.screenToX(e.offsetX);
2283 let y = this.screenToY(e.offsetY);
2284 // Guard against scale <= -1 otherwise this.xMin becomes larger than this.xMax
2285 let scale = Math.max(e.deltaY * 0.01, -0.9);
2286
2287 let xMinOld = this.xMin;
2288 let xMaxOld = this.xMax;
2289 let yMinOld = this.yMin;
2290 let yMaxOld = this.yMax;
2291
2292 if (this.param.xAxis.log) {
2293
2294 scale *= 10;
2295 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2296
2297 this.xMax *= 1 + scale * (1 - f);
2298 this.xMin /= 1 + scale * f;
2299
2300 if (this.xMax <= this.xMin) {
2301 this.xMin = xMinOld;
2302 this.xMax = xMaxOld;
2303 }
2304
2305 } else {
2306 let dx = (this.xMax - this.xMin) * scale;
2307 let f = (x - this.xMin) / (this.xMax - this.xMin);
2308 this.xMin = this.xMin - dx * f;
2309 this.xMax = this.xMax + dx * (1 - f);
2310 }
2311
2312 // avoid too high zoom (would kill axis rendering)
2313 if (this.xMax - this.xMin < 1E-10*(this.xMax0 - this.xMin0)) {
2314 this.xMin = xMinOld;
2315 this.xMax = xMaxOld;
2316 }
2317
2318 if (this.param.yAxis.log) {
2319
2320 scale *= 10;
2321 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
2322 let yMinOld = this.yMin;
2323 let yMaxOld = this.yMax;
2324
2325 this.yMax *= 1 + scale * f;
2326 this.yMin /= 1 + scale * (1 - f);
2327
2328 if (this.yMax <= this.yMin) {
2329 this.yMin = yMinOld;
2330 this.yMax = yMaxOld;
2331 }
2332
2333 } else {
2334 let dy = (this.yMax - this.yMin) * scale;
2335 let f = (y - this.yMin) / (this.yMax - this.yMin);
2336 this.yMin = this.yMin - dy * f;
2337 this.yMax = this.yMax + dy * (1 - f);
2338 }
2339
2340 // avoid too high zoom (would kill axis rendering)
2341 if (this.yMax - this.yMin < 1E-10*(this.yMax0 - this.yMin0)) {
2342 this.yMin = yMinOld;
2343 this.yMax = yMaxOld;
2344 }
2345
2346 this.blockAutoScale = true;
2347
2348 this.draw();
2349 }
2350
2351
2352 this.parentDiv.title = title;
2353 this.parentDiv.style.cursor = cursor;
2354
2355 e.preventDefault();
2356}
2357
2358MPlotGraph.prototype.resetAxes = function () {
2359 this.xMin = this.xMin0;
2360 this.xMax = this.xMax0;
2361 this.yMin = this.yMin0;
2362 this.yMax = this.yMax0;
2363
2364 this.blockAutoScale = false;
2365
2366 this.redraw();
2367}