3// Created by: Stefan Ritt
5// Contents: JavaScript graph plotting routines
7// Note: please load midas.js, mhttpd.js and control.js before mplot.js
12 showMenuButtons: true,
15 background: "#FFFFFF",
20 "#00AAFF", "#FF9000", "#FF00A0", "#00C030",
21 "#A0C0D0", "#D0A060", "#C04010", "#807060",
22 "#F0C000", "#2090A0", "#D040D0", "#90B000",
23 "#B0B040", "#B0B0FF", "#FFA0A0", "#A0FFA0"],
28 backgroundColor: "#808080",
36 backgroundColor: "#FFFFFF",
105function mplot_init() {
106 // go through all data-name="mplot" tags
107 let mPlot = document.getElementsByClassName("mplot");
109 for (let i = 0; i < mPlot.length; i++)
110 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
114 window.addEventListener('resize', windowResize);
117function profile(flag) {
118 if (flag === true || flag === undefined) {
120 profile.startTime = new Date().getTime();
124 let now = new Date().getTime();
125 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
126 profile.startTime = new Date().getTime();
129function windowResize() {
130 let mPlot = document.getElementsByClassName("mplot");
131 for (const m of mPlot)
135function isObject(item) {
136 return (item && typeof item === 'object' && !Array.isArray(item));
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]);
146 Object.assign(target, { [key]: source[key] });
153function MPlotGraph(divElement, param) { // Constructor
155 // save parameters from <div>
156 this.parentDiv = divElement;
157 this.divParam = divElement.innerHTML;
158 divElement.innerHTML = "";
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);
164 // default parameters
165 this.param = JSON.parse(JSON.stringify(defaultParam)); // deep copy
167 // overwrite default parameters from <div> text body
169 if (this.divParam.includes('{')) {
170 let p = JSON.parse(this.divParam);
171 this.param = deepMerge(this.param, p);
174 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
179 // obtain parameters form <div> attributes ---
182 if (this.parentDiv.dataset.odbPath)
183 for (let p of this.param.plot)
184 p.odbPath = this.parentDiv.dataset.odbPath;
187 if (this.parentDiv.dataset.title)
188 this.param.title.text = this.parentDiv.dataset.title;
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;
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;
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);
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;
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;
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>");
262 if (this.param.plot[0].ny === undefined) {
263 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
268 // data-x<n>/y<n>/label<n>/alpha<n>
269 for (let i=0 ; i<16 ; i++) {
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;
279 this.param.plot[index].x = this.parentDiv.dataset["x" + i];
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;
291 for (let i=0 ; i<16 ; i++) {
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;
301 this.param.plot[index].type = "histogram";
302 this.param.plot[index].y = this.parentDiv.dataset["h" + i];
304 this.param.plot[index].xMin = this.param.xAxis.min;
305 this.param.plot[index].xMax = this.param.xAxis.max;
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;
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('('));
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('('));
327 // set parameters from constructor
329 this.param.plot[0] = deepMerge(this.param.plot[0], param);
330 if (this.param.plot[0].type === "colormap") {
333 if (this.param.plot[0].nx === undefined) {
334 dlgAlert("\"nx\" missing in param for colormap mplot <div>");
337 if (this.param.plot[0].ny === undefined) {
338 dlgAlert("\"ny\" missing in param for colormap mplot <div>");
364 this.marker = {active: false};
365 this.blockAutoScale = false;
372 src: "rotate-ccw.svg",
373 title: "Reset histogram axes",
374 click: function (t) {
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 +
389 t.downloadSelector.style.zIndex = "32";
391 t.downloadSelector.style.display = "none";
397 this.button.forEach(b => {
399 b.img.src = "icons/" + b.src;
402 this.createDownloadSelector();
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);
411 // Keyboard event handler (has to be on the window!)
412 window.addEventListener("keydown", this.keyDown.bind(this));
415 this.canvas = document.createElement("canvas");
416 this.canvas.style.border = "1px solid black";
418 if (parseInt(this.parentDiv.style.width) > 0)
419 this.canvas.width = parseInt(this.parentDiv.style.width);
421 this.canvas.width = 500;
422 if (parseInt(this.parentDiv.style.height) > 0)
423 this.canvas.height = parseInt(this.parentDiv.style.height);
425 this.canvas.height = 300;
427 divElement.appendChild(this.canvas);
430MPlotGraph.prototype.createDownloadSelector = function () {
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";
444 this.downloadSelector.style.left = "100px";
445 this.downloadSelector.style.top = "100px";
447 let table = document.createElement("table");
450 let row = document.createElement("tr");
451 let cell = document.createElement("td");
452 cell.style.padding = "0";
453 let link = document.createElement("a");
455 link.innerHTML = "CSV";
456 link.title = "Download data in Comma Separated Value format";
457 link.onclick = function () {
458 mhg.downloadSelector.style.display = "none";
462 cell.appendChild(link);
463 row.appendChild(cell);
464 table.appendChild(row);
466 row = document.createElement("tr");
467 cell = document.createElement("td");
468 cell.style.padding = "0";
469 link = document.createElement("a");
471 link.innerHTML = "PNG";
472 link.title = "Download image in PNG format";
473 link.onclick = function () {
474 mhg.downloadSelector.style.display = "none";
478 cell.appendChild(link);
479 row.appendChild(cell);
480 table.appendChild(row);
482 this.downloadSelector.appendChild(table);
483 document.body.appendChild(this.downloadSelector);
486MPlotGraph.prototype.keyDown = function (e) {
488 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
494function loadMPlotData() {
496 // go through all data-name="mplot" tags
497 let mPlot = document.getElementsByClassName("mplot");
500 for (const mp of mPlot) {
501 for (const pl of mp.mpg.param.plot) {
502 if (pl.odbPath === undefined || pl.odbPath === "")
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+ "\"";
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 + "\"";
525 if (pl.odbPath.slice(-1) !== '/')
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);
537 mjsonrpc_db_get_values(v).then( function(rpc) {
539 let mPlot = document.getElementsByClassName("mplot");
541 for (let mp of mPlot) {
542 for (let p of mp.mpg.param.plot) {
543 if (!p.odbPath === undefined || p.odbPath === "" || p.invalid)
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 + "\"";
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 + "\"";
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 + "\"";
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) {
570 if (p.type === "scatter") {
571 // scatter plot goes from 0 ... N
573 p.xMax = p.yData.length;
574 p.xData = Array.from({length: p.yData.length}, (v, i) => i);
576 // histogram goes from -0.5 ... N-0.5 to have bins centered over bin x-value
578 p.xMax = p.yData.length - 0.5;
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);
585 if (p.xMin === undefined) {
586 p.xMin = Math.min(...p.xData);
587 p.xMax = Math.max(...p.xData);
591 p.yMin = Math.min(...p.yData);
592 p.yMax = Math.max(...p.yData);
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)));
599 if (p.xMin === undefined) {
603 if (p.yMin === undefined) {
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);
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);
619 for (const mp of mPlot) {
620 if (!mp.mpg.blockAutoScale)
625 // refresh data once per second
626 window.setTimeout(loadMPlotData, 1000);
628 }).catch( (error) => {
633MPlotGraph.prototype.setData = function (index, x, y) {
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 + "\"");
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";
652 p = this.param.plot[index];
654 p.odbPath = ""; // prevent loading of ODB data
656 if (p.type === "colormap") {
657 p.zData = x; // 2D array of colormap plot
661 for (const value of p.zData) {
663 if (typeof p.zMin === 'undefined' || p.zMin > value)
665 if (typeof p.zMax === 'undefined' || p.zMax < value)
670 if (p.xMin === undefined) {
674 if (p.yMin === undefined) {
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);
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);
689 if (p.type === "histogram") {
691 p.line.color = "#404040";
692 // generate X data for histograms
693 if (p.xMin === undefined || p.xMax === undefined) {
695 p.xMax = p.yData.length - 0.5;
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);
702 p.yMin = Math.min(...p.yData);
703 p.yMax = Math.max(...p.yData);
706 if (p.type === "scatter" ) {
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);
715 if (!this.blockAutoScale) {
722MPlotGraph.prototype.resize = function () {
723 this.canvas.width = this.parentDiv.clientWidth;
724 this.canvas.height = this.parentDiv.clientHeight;
729MPlotGraph.prototype.redraw = function () {
730 let f = this.draw.bind(this);
731 window.requestAnimationFrame(f);
734MPlotGraph.prototype.xToScreen = function (x) {
735 if (this.param.xAxis.log) {
739 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
740 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
742 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
745MPlotGraph.prototype.yToScreen = function (y) {
746 if (this.param.yAxis.log) {
750 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
751 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
753 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
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);
761 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
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);
769 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
772MPlotGraph.prototype.calcMinMax = function () {
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;
779 if (this.param.zAxis.min !== undefined)
780 this.zMin = this.param.zAxis.min;
782 this.zMin = this.param.plot[0].zMin;
784 if (this.param.zAxis.max !== undefined)
785 this.zMax = this.param.zAxis.max;
787 this.zMax = this.param.plot[0].zMax;
789 if (this.param.zAxis.log) {
790 if (this.zMin < 1E-20)
792 if (this.zMax < 1E-18)
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;
801 this.xMin0 = this.xMin;
802 this.xMax0 = this.xMax;
803 this.yMin0 = this.yMin;
804 this.yMax0 = this.yMax;
808 // determine min/max of overall plot
809 let xMin = this.param.plot[0].xMin;
810 for (const p of this.param.plot)
813 if (this.param.xAxis.min !== undefined)
814 xMin = this.param.xAxis.min;
816 let xMax = this.param.plot[0].xMax;
817 for (const p of this.param.plot)
820 if (this.param.xAxis.max !== undefined)
821 xMax = this.param.xAxis.max;
823 let yMin = this.param.plot[0].yMin;
824 for (const p of this.param.plot)
827 if (this.param.yAxis.min !== undefined)
828 yMin = this.param.yAxis.min;
830 let yMax = this.param.plot[0].yMax;
831 for (const p of this.param.plot)
834 if (this.param.yAxis.max !== undefined)
835 yMax = this.param.yAxis.max;
838 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
839 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
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)
847 if (this.param.xAxis.max === undefined)
849 if (this.param.yAxis.min === undefined)
852 if (this.param.yAxis.max === undefined)
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];
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];
877 let size = p.marker.size;
878 ctx.lineWidth = p.marker.lineWidth;
880 switch(p.marker.style) {
883 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
888 ctx.fillRect(x - size / 2, y - size / 2, size, size);
889 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
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);
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);
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);
921 case "triangle-down":
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);
930 case "triangle-left":
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);
939 case "triangle-right":
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);
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);
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);
967MPlotGraph.prototype.draw = function () {
974 let ctx = this.canvas.getContext("2d");
976 this.width = this.canvas.width;
977 this.height = this.canvas.height;
979 ctx.fillStyle = this.param.color.background;
980 ctx.fillRect(0, 0, this.width, this.height);
982 if (this.error !== null) {
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);
993 if (this.param.plot[0].xData === undefined) {
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);
1004 if (this.height === undefined || this.width === undefined)
1006 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1009 ctx.font = this.param.yAxis.textSize + "px sans-serif";
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);
1014 if (axisLabelWidth === undefined)
1017 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1018 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1020 this.x1 = axisLabelWidth + 15;
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;
1025 this.y1 = this.height - this.param.xAxis.textSize - 10;
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;
1033 if (this.param.showMenuButtons === false)
1034 this.x2 = this.width - 2;
1036 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1037 if (this.zMin === undefined || this.zMax === undefined) {
1041 if (this.zMin === this.zMax) {
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)
1052 if (this.param.zAxis.show) {
1053 let w = 5; // left gap
1054 w += 10; // color bar
1055 w += 12; // tick width
1058 this.x2 -= axisLabelWidth + w;
1059 this.param.zAxis.width = axisLabelWidth + w;
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);
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);
1083 if (this.param.yAxis.log && this.yMin < 1E-20)
1085 if (this.param.yAxis.log && this.yMax < 1E-18)
1088 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
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);
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);
1105 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
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);
1118 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
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);
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);
1136 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
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);
1149 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
1154 for (const p of this.param.plot) {
1155 if (p.xData === undefined || p.xData === null)
1158 if (p.xData.length > 0)
1161 ctx.globalAlpha = p.alpha;
1163 if (p.type === "scatter") {
1165 if (p.line && p.line.draw ||
1166 p.line && p.line.fill) {
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;
1176 ctx.globalAlpha = 0.1;
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]);
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));
1187 ctx.globalAlpha = 1;
1192 ctx.lineWidth = p.line.width;
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]);
1207 if (p.marker && p.marker.draw) {
1208 for (let i = 0; i < p.xData.length; i++) {
1210 let x = this.xToScreen(p.xData[i]);
1211 let y = this.yToScreen(p.yData[i]);
1213 this.drawMarker(ctx, p, x, y);
1218 else if (p.type === "histogram") {
1220 let dx = (p.xMax - p.xMin) / p.xData.length;
1221 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
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;
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);
1242 ctx.lineTo(x+dxs/2, this.yToScreen(0));
1243 ctx.globalAlpha = 0.2;
1245 ctx.globalAlpha = 1;
1249 else if (p.type === "colormap") {
1250 let dx = (p.xMax - p.xMin) / this.nx;
1251 let dy = (p.yMax - p.yMin) / this.ny;
1253 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1254 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
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];
1262 ctx.fillStyle = 'hsl(255, 0%, 50%)';
1265 if (this.param.zAxis.log) {
1269 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
1271 v = (zval - this.zMin) / (this.zMax - this.zMin);
1279 if (zval < 0.5 && this.param.plot[0].bgcolor)
1280 ctx.fillStyle = this.param.plot[0].bgcolor;
1282 ctx.fillStyle = 'hsl(' + Math.floor((1 - v) * 240) + ', 100%, 50%)';
1284 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
1291 ctx.restore(); // remove clipping
1294 if (this.param.plot[0].type === "colormap") {
1295 if (this.param.plot[0].showZScale) {
1297 for (let i=0 ; i<100 ; i++) {
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);
1306 ctx.strokeStyle = this.param.color.axis;
1308 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
1311 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1312 ctx.strokeStyle = this.param.color.axis;
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);
1321 for (const p of this.param.plot)
1322 if (p.label && p.label !== "")
1325 if (this.param.legend && this.param.legend.show && nLabel > 0) {
1326 ctx.font = this.param.legend.textSize + "px sans-serif";
1329 for (const p of this.param.plot) {
1330 if (ctx.measureText(p.label).width > mw) {
1331 mw = ctx.measureText(p.label).width;
1334 let w = 50 + mw + 5;
1335 let h = this.param.legend.textSize * 1.5;
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);
1342 for (const [pi,p] of this.param.plot.entries()) {
1345 ctx.strokeStyle = this.param.color.data[p.line.color];
1346 ctx.lineWidth = p.line.width;
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);
1353 this.drawMarker(ctx, p, this.x1 + 30, this.y2 + 10 + pi*h + h/2);
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);
1362 // "empty window" notice
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;
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";
1380 if (this.param.showMenuButtons) {
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;
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);
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);
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);
1421 if (this.marker.active) {
1424 if (this.param.plot[0].type !== "colormap") {
1426 ctx.globalAlpha = 0.1;
1427 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
1428 ctx.fillStyle = "#000000";
1430 ctx.globalAlpha = 1;
1433 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
1434 ctx.fillStyle = "#000000";
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);
1443 ctx.font = "12px sans-serif";
1444 ctx.textAlign = "left";
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('('));
1451 s = eval(f + "(this)");
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());
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;
1463 // move marker inside if outside plotting area
1464 if (x + w >= this.x2)
1465 x = this.marker.sx - 10 - w;
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);
1476 // call optional user overlay function
1477 if (this.param.overlay) {
1480 ctx.textAlign = "left";
1481 ctx.textBaseline = "top";
1482 ctx.fillStyle = "black";
1483 ctx.strokeStyle = "black";
1484 ctx.font = "12px sans-serif";
1486 eval(this.param.overlay + "(this, ctx)");
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];
1502 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
1505 if (xmax <= xmin || width <= 0)
1508 ctx.textAlign = "center";
1509 ctx.textBaseline = "top";
1513 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1514 if (isNaN(dx) || dx === 0) {
1522 } else { // linear axis ----
1524 // use 10 as min tick distance
1525 dx = (xmax - xmin) / (width / 10);
1527 int_dx = Math.floor(Math.log(dx) / LN10);
1528 frac_dx = Math.log(dx) / LN10 - int_dx;
1535 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
1536 major_base = label_base = tick_base + 1;
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;
1544 // number of significant digits
1548 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
1553 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
1555 n_sig1 = Math.max(n_sig1, n_sig2);
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);
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;
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);
1574 // increasing label_dx, if labels would overlap
1575 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1577 label_dx = Math.pow(10, int_dx) * base[label_base];
1578 if (label_base % 3 === 2 && major_base % 3 === 1) {
1580 major_dx = Math.pow(10, int_dx) * base[major_base];
1588 x_act = Math.floor(xmin / dx) * dx;
1590 ctx.strokeStyle = this.param.color.axis;
1591 ctx.drawLine(x1, y1, x1 + width, y1);
1595 x_screen = (Math.log(x_act) - Math.log(xmin)) /
1596 (Math.log(xmax) - Math.log(xmin)) * width + x1;
1598 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1599 xs = Math.floor(x_screen + 0.5);
1601 if (x_screen > x1 + width + 0.001)
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) {
1608 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1609 dx / label_dx / 10.0) {
1611 ctx.strokeStyle = this.param.color.axis;
1612 ctx.drawLine(xs, y1, xs, y1 + text);
1615 if (grid !== 0 && xs > x1 && xs < x1 + width) {
1616 ctx.strokeStyle = this.param.color.grid;
1617 ctx.drawLine(xs, y1, xs, y1 + grid);
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);
1630 last_label_x = xs + ext.width / 2;
1634 ctx.strokeStyle = this.param.color.axis;
1635 ctx.drawLine(xs, y1, xs, y1 + major);
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);
1652 ctx.strokeStyle = this.param.color.axis;
1653 ctx.drawLine(xs, y1, xs, y1 + minor);
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);
1663 // for log axis, also put labels on minor tick marks
1666 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
1667 str = x_act.toExponential(n_sig1).stripZeros();
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);
1679 last_label_x = xs + ext.width / 2;
1686 /* suppress 1.23E-17 ... */
1687 if (Math.abs(x_act) < dx / 100)
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];
1700 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
1703 if (ymax <= ymin || height <= 0)
1707 ctx.textAlign = "right";
1709 ctx.textAlign = "left";
1710 ctx.textBaseline = "middle";
1711 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1713 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
1714 dy = Number.MAX_VALUE / 10;
1718 } else if (logaxis) {
1719 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1720 if (isNaN(dy) || dy === 0) {
1728 // use 6 as min tick distance
1729 dy = (ymax - ymin) / (height / 6);
1731 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1732 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
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;
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;
1747 // number of significant digits
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;
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;
1760 n_sig1 = Math.max(n_sig1, n_sig2);
1761 n_sig1 = Math.max(1, n_sig1);
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);
1771 // increase label_dy if labels would overlap
1772 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1774 label_dy = Math.pow(10, int_dy) * base[label_base];
1775 if (label_base % 3 === 2 && major_base % 3 === 1) {
1777 major_dy = Math.pow(10, int_dy) * base[major_base];
1782 y_act = Math.floor(ymin / dy) * dy;
1784 let last_label_y = y1;
1788 ctx.strokeStyle = this.param.color.axis;
1789 ctx.drawLine(x1, y1, x1, y1 - height);
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;
1799 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1800 ys = Math.round(y_screen);
1802 if (y_screen < y1 - height - 0.001 || isNaN(ys))
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) {
1809 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
1810 dy / label_dy / 10.0) {
1813 ctx.strokeStyle = this.param.color.axis;
1814 ctx.drawLine(x1, ys, x1 + text, ys);
1818 if (grid !== 0 && ys < y1 && ys > y1 - height)
1820 ctx.strokeStyle = this.param.color.grid;
1821 ctx.drawLine(x1, ys, x1 + grid, ys);
1827 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
1828 str = y_act.toExponential(n_sig1).stripZeros();
1830 str = y_act.toPrecision(n_sig1).stripZeros();
1831 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
1833 ctx.strokeStyle = this.param.color.label;
1834 ctx.fillStyle = this.param.color.label;
1835 ctx.fillText(str, x1 + label, ys);
1837 last_label_y = ys - textHeight / 2;
1842 ctx.strokeStyle = this.param.color.axis;
1843 ctx.drawLine(x1, ys, x1 + major, ys);
1847 if (grid !== 0 && ys < y1 && ys > y1 - height)
1849 ctx.strokeStyle = this.param.color.grid;
1850 ctx.drawLine(x1, ys, x1 + grid, ys);
1863 ctx.strokeStyle = this.param.color.axis;
1864 ctx.drawLine(x1, ys, x1 + minor, ys);
1870 // for log axis, also put grid lines on minor tick marks
1871 if (grid !== 0 && ys < y1 && ys > y1 - height) {
1873 ctx.strokeStyle = this.param.color.grid;
1874 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
1878 // for log axis, also put labels on minor tick marks
1881 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
1882 str = y_act.toExponential(n_sig1).stripZeros();
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);
1890 ctx.strokeStyle = this.param.color.label;
1891 ctx.fillStyle = this.param.color.label;
1892 ctx.fillText(str, x1 + label, ys);
1903 // suppress 1.23E-17 ...
1904 if (Math.abs(y_act) < dy / 100)
1912MPlotGraph.prototype.download = function (mode) {
1915 let filename = this.param.title.text + "-" +
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);
1923 // use trick from FileSaver.js
1924 let a = document.getElementById('downloadHook');
1926 a = document.createElement("a");
1927 a.style.display = "none";
1928 a.id = "downloadHook";
1929 document.body.appendChild(a);
1932 if (mode === "CSV") {
1938 this.param.plot.forEach(p => {
1939 if (p.type === "scatter" || p.type === "histogram") {
1948 for (let i = 0; i < p.xData.length; i++) {
1949 data += p.xData[i] + ",";
1950 data += p.yData[i] + "\n";
1955 if (p.type === "colormap") {
1959 for (let i = 0; i < p.nx; i++)
1960 data += p.xData[i] + ",";
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] + ",";
1972 let blob = new Blob([data], {type: "text/csv"});
1973 let url = window.URL.createObjectURL(blob);
1976 a.download = filename;
1978 window.URL.revokeObjectURL(url);
1979 dlgAlert("Data downloaded to '" + filename + "'");
1981 } else if (mode === "PNG") {
1984 let smb = this.param.showMenuButtons;
1985 this.param.showMenuButtons = false;
1989 this.canvas.toBlob(function (blob) {
1990 let url = window.URL.createObjectURL(blob);
1993 a.download = filename;
1995 window.URL.revokeObjectURL(url);
1996 dlgAlert("Image downloaded to '" + filename + "'");
1998 h.param.showMenuButtons = smb;
2006MPlotGraph.prototype.drawTextBox = function (ctx, text, x, y) {
2007 let line = text.split("\n");
2010 for (const p of line)
2011 if (ctx.measureText(p).width > mw)
2012 mw = ctx.measureText(p).width;
2014 let h = parseInt(ctx.font) * 1.5;
2016 let c = ctx.fillStyle;
2017 ctx.fillStyle = "white";
2018 ctx.fillRect(x, y, w, h * line.length);
2020 ctx.strokeRect(x, y, w, h * line.length);
2022 for (let i=0 ; i<line.length ; i++)
2023 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
2026MPlotGraph.prototype.mouseEvent = function (e) {
2028 // execute callback if registered
2029 if (this.param.event) {
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);
2046 let flag = eval(this.param.event + "(e, this, ix, iy)");
2052 // call all other plots only with event and object
2053 let flag = eval(this.param.event + "(e, this)");
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
2067 let cursor = "default";
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))
2076 if (e.type === "mousedown") {
2078 this.downloadSelector.style.display = "none";
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 &&
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;
2103 this.blockAutoScale = true;
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);
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);
2120 } else if (cancel || e.type === "mouseup") {
2122 if (this.drag.active)
2123 this.drag.active = false;
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);
2131 [x1, x2] = [x2, x1];
2135 this.zoom.x.active = false;
2136 this.blockAutoScale = true;
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);
2146 [y1, y2] = [y2, y1];
2150 this.zoom.y.active = false;
2151 this.blockAutoScale = true;
2155 } else if (e.type === "mousemove") {
2157 if (this.drag.active) {
2162 if (this.param.xAxis.log) {
2163 let dx = e.offsetX - this.drag.sxStart;
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));
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;
2178 if (this.param.yAxis.log) {
2179 let dy = e.offsetY - this.drag.syStart;
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));
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;
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) {
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);
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);
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)
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);
2233 this.marker.x = p.xData[i];
2234 this.marker.y = p.yData[i];
2237 this.marker.mx = e.offsetX;
2238 this.marker.my = e.offsetY;
2239 this.marker.plotIndex = pi;
2240 this.marker.index = i;
2245 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
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);
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];
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;
2273 this.marker.active = false;
2280 } else if (e.type === "wheel") {
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);
2287 let xMinOld = this.xMin;
2288 let xMaxOld = this.xMax;
2289 let yMinOld = this.yMin;
2290 let yMaxOld = this.yMax;
2292 if (this.param.xAxis.log) {
2295 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
2297 this.xMax *= 1 + scale * (1 - f);
2298 this.xMin /= 1 + scale * f;
2300 if (this.xMax <= this.xMin) {
2301 this.xMin = xMinOld;
2302 this.xMax = xMaxOld;
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);
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;
2318 if (this.param.yAxis.log) {
2321 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
2322 let yMinOld = this.yMin;
2323 let yMaxOld = this.yMax;
2325 this.yMax *= 1 + scale * f;
2326 this.yMin /= 1 + scale * (1 - f);
2328 if (this.yMax <= this.yMin) {
2329 this.yMin = yMinOld;
2330 this.yMax = yMaxOld;
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);
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;
2346 this.blockAutoScale = true;
2352 this.parentDiv.title = title;
2353 this.parentDiv.style.cursor = cursor;
2358MPlotGraph.prototype.resetAxes = function () {
2359 this.xMin = this.xMin0;
2360 this.xMax = this.xMax0;
2361 this.yMin = this.yMin0;
2362 this.yMax = this.yMax0;
2364 this.blockAutoScale = false;