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",
42 type: "numeric", // One of "numeric", "datetime", "category"
83 type: "scatter", // One of "scatter", "histogram", "colormap"
95 style: "circle", // One of "none", "circle", "square", "diamond", "pentagon", "triangle-up", "triangle-down", "triangle-left", "triangle-right", "cross", "plus"
104 style: "solid", // One of "none", "solid"
111function mplot_init() {
113 * Initialize <div> objects of class mplot
114 * Can only be called after all data has been created
117 // go through all data-name="mplot" tags
118 let mPlot = document.getElementsByClassName("mplot");
120 for (let i = 0; i < mPlot.length; i++)
121 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
125 window.addEventListener('resize', windowResize);
128function profile(flag) {
129 if (flag === true || flag === undefined) {
131 profile.startTime = new Date().getTime();
135 let now = new Date().getTime();
136 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
137 profile.startTime = new Date().getTime();
140function windowResize() {
142 * Resize all mplot objects as defined by their class
144 let mPlot = document.getElementsByClassName("mplot");
145 for (const m of mPlot)
149function isObject(item) {
150 return (item && typeof item === 'object' && !Array.isArray(item));
153function deepMerge(target, source) {
155 * Recursively merge object keys from source into target. Modifies in-place.
156 * @param {object} target the object to make new keys or overwrite existing ones
157 * @param {object} source the object from which the keys are copied
158 * @returns {object} the target with the new keys
160 for (let key in source) {
161 if (source.hasOwnProperty(key)) {
162 if (isObject(source[key])) {
163 if (!target[key]) Object.assign(target, { [key]: {} });
164 deepMerge(target[key], source[key]);
166 Object.assign(target, { [key]: source[key] });
173function MPlotGraph(divElement, param) { // Constructor
175 * MPlotGraph Constructor
176 * @param {div element} divElement HTML <div> element to place the plot into
177 @param {object} param JS object with keys a subset of DefaultParam, controls plot display style
180 // save parameters from <div>
181 this.parentDiv = divElement;
182 this.divParam = divElement.innerHTML;
183 divElement.innerHTML = "";
185 // if absent, generate random string (5 char) to give an id to parent element
186 if (!this.parentDiv.id)
187 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
189 // default parameters
190 this.param = JSON.parse(JSON.stringify(defaultParam)); // deep copy
192 // overwrite default parameters from <div> text body
194 if (this.divParam.includes('{')) {
195 let p = JSON.parse(this.divParam);
196 this.param = deepMerge(this.param, p);
199 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
204 // obtain parameters form <div> attributes ---
207 if (this.parentDiv.dataset.odbPath)
208 for (let p of this.param.plot)
209 p.odbPath = this.parentDiv.dataset.odbPath;
212 if (this.parentDiv.dataset.type)
213 this.param.type = this.parentDiv.dataset.type;
216 if (this.parentDiv.dataset.title)
217 this.param.title.text = this.parentDiv.dataset.title;
220 if (this.parentDiv.dataset.xText)
221 this.param.xAxis.title.text =this.parentDiv.dataset.xText;
222 if (this.parentDiv.dataset.yText)
223 this.param.yAxis.title.text =this.parentDiv.dataset.yText;
224 if (this.parentDiv.dataset.zText)
225 this.param.zAxis.title.text =this.parentDiv.dataset.zText;
228 if (this.parentDiv.dataset.x)
229 this.param.plot[0].x = this.parentDiv.dataset.x;
230 if (this.parentDiv.dataset.y)
231 this.param.plot[0].y = this.parentDiv.dataset.y;
234 if (this.parentDiv.dataset.xError)
235 this.param.plot[0].xError = this.parentDiv.dataset.xError;
236 if (this.parentDiv.dataset.yError)
237 this.param.plot[0].yError = this.parentDiv.dataset.yError;
239 // data-x/y/z-min/max
240 if (this.parentDiv.dataset.xMin)
241 this.param.xAxis.min = parseFloat(this.parentDiv.dataset.xMin);
242 if (this.parentDiv.dataset.xMax)
243 this.param.xAxis.max = parseFloat(this.parentDiv.dataset.xMax);
244 if (this.parentDiv.dataset.yMin)
245 this.param.yAxis.min = parseFloat(this.parentDiv.dataset.yMin);
246 if (this.parentDiv.dataset.yMax)
247 this.param.yAxis.max = parseFloat(this.parentDiv.dataset.yMax);
248 if (this.parentDiv.dataset.zMin)
249 this.param.zAxis.min = parseFloat(this.parentDiv.dataset.zMin);
250 if (this.parentDiv.dataset.zMax)
251 this.param.zAxis.max = parseFloat(this.parentDiv.dataset.zMax);
254 if (this.parentDiv.dataset.xLog)
255 this.param.xAxis.log = this.parentDiv.dataset.xLog === "true" || this.parentDiv.dataset.xLog === "1";
256 if (this.parentDiv.dataset.yLog)
257 this.param.yAxis.log = this.parentDiv.dataset.yLog === "true" || this.parentDiv.dataset.yLog === "1";
258 if (this.parentDiv.dataset.zLog) {
259 this.param.zAxis.log = this.parentDiv.dataset.zLog === "true" || this.parentDiv.dataset.zLog === "1";
260 if (this.param.zAxis.log) {
261 if (this.param.zAxis.min < 1E-20)
262 this.param.zAxis.min = 1E-20;
263 if (this.param.zAxis.max < 1E-18)
264 this.param.zAxis.max = 1E-18;
269 if (this.parentDiv.dataset.h) {
270 this.param.plot[0].type = "histogram";
271 this.param.plot[0].y = this.parentDiv.dataset.h;
272 this.param.plot[0].line.color = "#404040";
273 if (!this.parentDiv.dataset.x) {
274 this.param.plot[0].xMin = this.param.xAxis.min;
275 this.param.plot[0].xMax = this.param.xAxis.max;
280 if (this.parentDiv.dataset.z) {
281 this.param.plot[0].type = "colormap";
282 this.param.plot[0].showZScale = true;
283 this.param.plot[0].bgcolor = this.parentDiv.dataset.bgcolor;
284 this.param.plot[0].z = this.parentDiv.dataset.z;
285 this.param.plot[0].xMin = this.param.xAxis.min;
286 this.param.plot[0].xMax = this.param.xAxis.max;
287 this.param.plot[0].yMin = this.param.yAxis.min;
288 this.param.plot[0].yMax = this.param.yAxis.max;
289 this.param.plot[0].zMin = this.param.zAxis.min;
290 this.param.plot[0].zMax = this.param.zAxis.max;
291 this.param.plot[0].nx = parseInt(this.parentDiv.dataset.nx);
292 this.param.plot[0].ny = parseInt(this.parentDiv.dataset.ny);
293 if (this.param.plot[0].nx === undefined) {
294 dlgAlert("\"data-nx\" missing for colormap mplot <div>");
297 if (this.param.plot[0].ny === undefined) {
298 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
304 if (this.parentDiv.dataset["line-width"])
305 this.param.plot[0].line.width = this.parentDiv.dataset["line-width"];
308 if (this.parentDiv.dataset["bar-width"])
309 this.param.plot[0].barWidth = this.parentDiv.dataset["bar-width"];
312 if (this.parentDiv.dataset["marker-style"])
313 this.param.plot[0].marker.style = this.parentDiv.dataset["marker-style"];
315 // data-x<n>/y<n>/label<n>/alpha<n>/marker<n>
316 for (let i=0 ; i<16 ; i++) {
318 if (this.parentDiv.dataset["x"+i]) {
319 if (this.param.plot[0].x !== "") {
320 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
321 index = this.param.plot.length-1;
322 this.param.plot[index].marker.lineColor = index;
323 this.param.plot[index].marker.fillColor = index;
324 this.param.plot[index].line.color = index;
326 this.param.plot[index].x = this.parentDiv.dataset["x" + i];
328 if (this.parentDiv.dataset["y"+i])
329 this.param.plot[index].y = this.parentDiv.dataset["y"+i];
330 if (this.parentDiv.dataset["x"+i+"Error"])
331 this.param.plot[index].xError = this.parentDiv.dataset["x"+i+"Error"];
332 if (this.parentDiv.dataset["y"+i+"Error"])
333 this.param.plot[index].yError = this.parentDiv.dataset["y"+i+"Error"];
334 if (this.parentDiv.dataset["label"+i])
335 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
336 if (this.parentDiv.dataset["alpha"+i])
337 this.param.plot[index].alpha = parseFloat(this.parentDiv.dataset["alpha"+i]);
338 if (this.parentDiv.dataset["line"+i+"-width"])
339 this.param.plot[index].line.width = parseFloat(this.parentDiv.dataset["line"+i+"-width"]);
340 if (this.parentDiv.dataset["marker"+i+"-style"])
341 this.param.plot[index].marker.style = this.parentDiv.dataset["marker"+i+"-style"];
342 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
346 for (let i=0 ; i<16 ; i++) {
348 if (this.parentDiv.dataset["h"+i]) {
349 if (this.param.plot[0].y !== "") {
350 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
351 index = this.param.plot.length-1;
352 this.param.plot[index].marker.lineColor = index;
353 this.param.plot[index].marker.fillColor = index;
354 this.param.plot[index].line.color = index;
356 this.param.plot[index].type = "histogram";
357 this.param.plot[index].y = this.parentDiv.dataset["h" + i];
359 this.param.plot[index].xMin = this.param.xAxis.min;
360 this.param.plot[index].xMax = this.param.xAxis.max;
362 if (this.parentDiv.dataset["label"+i])
363 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
364 this.param.plot[index].odbPath = this.param.plot[0].odbPath;
369 if (this.parentDiv.dataset["x-type"])
370 this.param.xAxis.type = this.parentDiv.dataset["x-type"];
373 if (this.parentDiv.dataset.overlay) {
374 this.param.overlay = this.parentDiv.dataset.overlay;
375 if (this.param.overlay.indexOf('(') !== -1) // strip any '('
376 this.param.overlay = this.param.overlay.substring(0, this.param.overlay.indexOf('('));
380 if (this.parentDiv.dataset.event) {
381 this.param.event = this.parentDiv.dataset.event;
382 if (this.param.event.indexOf('(') !== -1) // strip any '('
383 this.param.event = this.param.event.substring(0, this.param.event.indexOf('('));
387 if (this.parentDiv.dataset.stats)
388 this.param.stats.show = (this.parentDiv.dataset.stats === "1");
390 // set parameters from constructor
392 deepMerge(this.param, param);
393 if (this.param.plot[0].type === "colormap") {
396 if (this.param.plot[0].nx === undefined) {
397 dlgAlert("\"nx\" missing in param for colormap mplot <div>");
400 if (this.param.plot[0].ny === undefined) {
401 dlgAlert("\"ny\" missing in param for colormap mplot <div>");
427 this.marker = {active: false};
428 this.blockAutoScale = false;
436 title: "Show / hide legend",
437 click: function (t) {
438 t.param.legend.show = !t.param.legend.show;
444 title: "Show / hide statistics",
445 click: function (t) {
446 t.param.stats.show = !t.param.stats.show;
451 src: "rotate-ccw.svg",
452 title: "Reset histogram axes",
453 click: function (t) {
459 title: "Download image/data...",
460 click: function (t) {
461 if (t.downloadSelector.style.display === "none") {
462 t.downloadSelector.style.display = "block";
463 let w = t.downloadSelector.getBoundingClientRect().width;
464 t.downloadSelector.style.left = (t.canvas.getBoundingClientRect().x + window.scrollX +
465 t.width - 26 - w) + "px";
466 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.scrollY +
468 t.downloadSelector.style.zIndex = "32";
470 t.downloadSelector.style.display = "none";
476 this.button.forEach(b => {
478 b.img.src = "icons/" + b.src;
481 this.createDownloadSelector();
483 // mouse event handlers
484 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
485 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
486 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
487 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
488 divElement.addEventListener("wheel", this.mouseEvent.bind(this), true);
490 // Keyboard event handler (has to be on the window!)
491 window.addEventListener("keydown", this.keyDown.bind(this));
494 this.canvas = document.createElement("canvas");
495 this.canvas.style.border = "1px solid black";
497 if (parseInt(this.parentDiv.style.width) > 0)
498 this.canvas.width = parseInt(this.parentDiv.style.width);
500 this.canvas.width = 500;
501 if (parseInt(this.parentDiv.style.height) > 0)
502 this.canvas.height = parseInt(this.parentDiv.style.height);
504 this.canvas.height = 300;
506 divElement.appendChild(this.canvas);
509MPlotGraph.prototype.createDownloadSelector = function () {
511 * Make a download section menu
515 let downloadSelId = this.parentDiv.id + "downloadSel";
516 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
517 this.downloadSelector = document.createElement("div");
518 this.downloadSelector.id = downloadSelId;
519 this.downloadSelector.style.display = "none";
520 this.downloadSelector.style.position = "absolute";
521 this.downloadSelector.className = "mtable";
522 this.downloadSelector.style.borderRadius = "0";
523 this.downloadSelector.style.border = "2px solid #808080";
524 this.downloadSelector.style.margin = "0";
525 this.downloadSelector.style.padding = "0";
527 this.downloadSelector.style.left = "100px";
528 this.downloadSelector.style.top = "100px";
530 let table = document.createElement("table");
533 let row = document.createElement("tr");
534 let cell = document.createElement("td");
535 cell.style.padding = "0";
536 let link = document.createElement("a");
538 link.innerHTML = "CSV";
539 link.title = "Download data in Comma Separated Value format";
540 link.onclick = function () {
541 mhg.downloadSelector.style.display = "none";
545 cell.appendChild(link);
546 row.appendChild(cell);
547 table.appendChild(row);
549 row = document.createElement("tr");
550 cell = document.createElement("td");
551 cell.style.padding = "0";
552 link = document.createElement("a");
554 link.innerHTML = "PNG";
555 link.title = "Download image in PNG format";
556 link.onclick = function () {
557 mhg.downloadSelector.style.display = "none";
561 cell.appendChild(link);
562 row.appendChild(cell);
563 table.appendChild(row);
565 this.downloadSelector.appendChild(table);
566 document.body.appendChild(this.downloadSelector);
569MPlotGraph.prototype.keyDown = function (e) {
572 * @param {Object} e keydown event with properties key, metaKey, ctrlKey, target, etc
574 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
576 // don't grab key if we are in an input field
577 if (e.target.tagName === "INPUT")
585function loadMPlotData() {
587 * Load data from the ODB for all HTML elements with the mplot class
590 // go through all data-name="mplot" tags
591 let mPlot = document.getElementsByClassName("mplot");
594 for (const mp of mPlot) {
595 for (const pl of mp.mpg.param.plot) {
596 if (pl.odbPath === undefined || pl.odbPath === "")
603 if ((pl.type === "scatter" || pl.type === "histogram") &&
604 (pl.y === undefined || pl.y === null || pl.y === "")) {
605 mp.mpg.error ="Invalid Y data \"" + pl.y + "\" for " + pl.type + " plot \"" + name+ "\"";
611 if ((pl.type === "colormap") &&
612 (pl.z === undefined || pl.z === null || pl.z === "")) {
613 mp.mpg.error = "Invalid Z data \"" + pl.y + "\" for colormap plot \"" + name + "\"";
619 if (pl.odbPath.slice(-1) !== '/')
622 if (pl.x !== undefined && pl.x !== null && pl.x !== "")
623 v.push(pl.odbPath + pl.x);
624 if (pl.y !== undefined && pl.y !== null && pl.y !== "")
625 v.push(pl.odbPath + pl.y);
626 if (pl.z !== undefined && pl.z !== null && pl.z !== "")
627 v.push(pl.odbPath + pl.z);
628 if (pl.xError !== undefined && pl.xError !== null && pl.xError !== "")
629 v.push(pl.odbPath + pl.xError);
630 if (pl.yError !== undefined && pl.yError !== null && pl.yError !== "")
631 v.push(pl.odbPath + pl.yError);
635 mjsonrpc_db_get_values(v).then( function(rpc) {
637 let mPlot = document.getElementsByClassName("mplot");
640 for (let mp of mPlot) {
641 for (let p of mp.mpg.param.plot) {
642 if (!p.odbPath === undefined || p.odbPath === "" || p.invalid)
649 if (p.x !== undefined && p.x !== null && p.x !== "") {
650 p.xData = rpc.result.data[i++];
651 if (p.xData === null)
652 mp.mpg.error = "Invalid X data \"" + p.x + "\" for plot \"" + name + "\"";
653 if (Array.isArray(p.xData) && p.xData.length > 0 &&
654 p.xData.every(item => typeof item === "string")) {
656 // switch plot to category plot if sting array found for x-data
658 p.marker = undefined;
659 mp.mpg.param.xAxis.type = "category";
660 mp.mpg.param.stats.show = false;
663 if (p.y !== undefined && p.y !== null && p.y !== "") {
664 p.yData = rpc.result.data[i++];
665 if (p.yData === null)
666 mp.mpg.error = "Invalid Y data \"" + p.y + "\" for plot \"" + name + "\"";
668 if (p.z !== undefined && p.z !== null && p.z !== "") {
669 p.zData = rpc.result.data[i++];
670 if (p.zData === null)
671 mp.mpg.error = "Invalid Z data \"" + p.z + "\" for plot \"" + name + "\"";
673 if (p.xError !== undefined && p.xError !== null && p.xError !== "") {
674 p.xErrorData = rpc.result.data[i++];
675 if (p.xErrorData === null)
676 mp.mpg.error = "Invalid X error data \"" + p.xError + "\" for plot \"" + name + "\"";
678 if (p.yError !== undefined && p.yError !== null && p.yError !== "") {
679 p.yErrorData = rpc.result.data[i++];
680 if (p.yErrorData === null)
681 mp.mpg.error = "Invalid Y error data \"" + p.yError + "\" for plot \"" + name + "\"";
684 if ((p.type === "scatter" || p.type === "histogram" || p.type === "category") &&
685 mp.mpg.error === null) {
686 // generate X data for histograms
687 if (p.xData === undefined || p.xData === null) {
689 if (p.type === "scatter") {
690 // scatter plot goes from 0 ... N
692 p.xMax = p.yData.length;
693 p.xData = Array.from({length: p.yData.length}, (v, i) => i);
695 // histogram goes from -0.5 ... N-0.5 to have bins centered over bin x-value
697 p.xMax = p.yData.length - 0.5;
699 let dx = (p.xMax - p.xMin) / p.yData.length;
700 let x0 = p.xMin + dx / 2;
701 p.xData = Array.from({length: p.yData.length}, (v, i) => x0 + i * dx);
704 if (p.type === "category") {
706 p.xMax = p.xData.length;
707 } else if (p.xMin === undefined) {
708 p.xMin = Math.min(...p.xData);
709 p.xMax = Math.max(...p.xData);
713 p.yMin = Math.min(...p.yData);
714 p.yMax = Math.max(...p.yData);
716 if (p.type === "category")
720 if (p.type === "colormap" && mp.mpg.error === null) {
721 p.zMin = Math.min(...p.zData.filter(v=>!isNaN(v)));
722 p.zMax = Math.max(...p.zData.filter(v=>!isNaN(v)));
724 if (p.xMin === undefined) {
728 if (p.yMin === undefined) {
733 let dx = (p.xMax - p.xMin) / p.nx;
734 let x0 = p.xMin + dx/2;
735 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
737 let dy = (p.yMax - p.yMin) / p.ny;
738 let y0 = p.yMin + dy/2;
739 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
746 for (const mp of mPlot) {
747 if (!mp.mpg.blockAutoScale)
752 // refresh data once per second
753 window.setTimeout(loadMPlotData, 1000);
755 }).catch( (error) => {
760MPlotGraph.prototype.setData = function (index, x, y) {
762 * Set the data for the mplot
763 * @param {int} index Choose which data series to edit or add. Allows multiple data sets per plot
764 * @param {array} x values for the x axis
765 * @param {array} y values for the y axis
768 if (index > this.param.plot.length) {
769 dlgAlert("Wrong index \"" + index + "\" for graph \""+ this.param.title.text +"\"<br />" +
770 "New index must be \"" + this.param.plot.length + "\"");
776 if (index + 1 > this.param.plot.length) {
777 // add new default plot
778 this.param.plot.push(JSON.parse(JSON.stringify(defaultParam.plot[0])));
779 p = this.param.plot[index];
780 p.marker.lineColor = index;
781 p.marker.fillColor = index;
782 p.line.color = index;
783 p.type = y ? "scatter" : "histogram";
785 p = this.param.plot[index];
787 p.odbPath = ""; // prevent loading of ODB data
789 if (p.type === "colormap") {
790 p.zData = x; // 2D array of colormap plot
794 for (const value of p.zData) {
796 if (typeof p.zMin === 'undefined' || p.zMin > value)
798 if (typeof p.zMax === 'undefined' || p.zMax < value)
803 if (p.xMin === undefined) {
807 if (p.yMin === undefined) {
812 let dx = (p.xMax - p.xMin) / p.nx;
813 let x0 = p.xMin + dx/2;
814 p.xData = Array.from({length: p.nx}, (v,i) => x0 + i*dx);
816 let dy = (p.yMax - p.yMin) / p.ny;
817 let y0 = p.yMin + dy/2;
818 p.yData = Array.from({length: p.ny}, (v,i) => y0 + i*dy);
822 if (p.type === "histogram") {
824 p.line.color = "#404040";
825 // generate X data for histograms
826 if (p.xMin === undefined || p.xMax === undefined) {
828 p.xMax = p.yData.length - 0.5;
830 let dx = (p.xMax - p.xMin) / p.yData.length;
831 let x0 = p.xMin + dx/2;
832 if (p.xData === undefined || p.xData === null)
833 p.xData = Array.from({length: p.yData.length}, (v,i) => x0 + i*dx);
835 p.yMin = Math.min(...p.yData);
836 p.yMax = Math.max(...p.yData);
839 if (p.type === "category") {
846 p.yMax = Math.max(...p.yData);
849 if (p.type === "scatter" ) {
852 p.xMin = Math.min(...p.xData);
853 p.xMax = Math.max(...p.xData);
854 p.yMin = Math.min(...p.yData);
855 p.yMax = Math.max(...p.yData);
858 if (!this.blockAutoScale) {
865MPlotGraph.prototype.resize = function () {
867 * Resize canvas and redraw
869 this.canvas.width = this.parentDiv.clientWidth;
870 this.canvas.height = this.parentDiv.clientHeight;
875MPlotGraph.prototype.redraw = function () {
876 let f = this.draw.bind(this);
877 window.requestAnimationFrame(f);
880MPlotGraph.prototype.xToScreen = function (x) {
882 * Convert data coordinates to screen coordinates x axis
885 if (this.param.xAxis.log) {
889 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
890 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
892 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
895MPlotGraph.prototype.yToScreen = function (y) {
897 * Convert data coordinates to screen coordinates y axis
899 if (this.param.yAxis.log) {
903 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
904 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
906 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
909MPlotGraph.prototype.screenToX = function (x) {
911 * Convert screen coordinates to data coordinates x axis
913 if (this.param.xAxis.log) {
914 let xl = (x - this.x1) / (this.x2 - this.x1) * (Math.log(this.xMax)-Math.log(this.xMin)) + Math.log(this.xMin);
917 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
920MPlotGraph.prototype.screenToY = function (y) {
922 * Convert screen coordinates to data coordinates y axis
924 if (this.param.yAxis.log) {
925 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
928 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
931MPlotGraph.prototype.calcMinMax = function () {
933 // simple nx / ny for colormaps
934 if (this.param.plot[0].type === "colormap") {
935 this.nx = this.param.plot[0].nx;
936 this.ny = this.param.plot[0].ny;
938 if (this.param.zAxis.min !== undefined)
939 this.zMin = this.param.zAxis.min;
941 this.zMin = this.param.plot[0].zMin;
943 if (this.param.zAxis.max !== undefined)
944 this.zMax = this.param.zAxis.max;
946 this.zMax = this.param.plot[0].zMax;
948 if (this.param.zAxis.log) {
949 if (this.zMin < 1E-20)
951 if (this.zMax < 1E-18)
955 this.xMin = this.param.plot[0].xMin;
956 this.xMax = this.param.plot[0].xMax;
957 this.yMin = this.param.plot[0].yMin;
958 this.yMax = this.param.plot[0].yMax;
960 this.xMin0 = this.xMin;
961 this.xMax0 = this.xMax;
962 this.yMin0 = this.yMin;
963 this.yMax0 = this.yMax;
967 // determine min/max of overall plot
968 let xMin = this.param.plot[0].xMin;
969 for (const p of this.param.plot)
972 if (this.param.xAxis.min !== undefined)
973 xMin = this.param.xAxis.min;
975 let xMax = this.param.plot[0].xMax;
976 for (const p of this.param.plot)
979 if (this.param.xAxis.max !== undefined)
980 xMax = this.param.xAxis.max;
982 let yMin = this.param.plot[0].yMin;
983 for (const p of this.param.plot)
986 if (this.param.yAxis.min !== undefined)
987 yMin = this.param.yAxis.min;
989 let yMax = this.param.plot[0].yMax;
990 for (const p of this.param.plot)
993 if (this.param.yAxis.max !== undefined)
994 yMax = this.param.yAxis.max;
997 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
998 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
1000 // add 5% on each side
1001 let dx = (xMax - xMin);
1002 let dy = (yMax - yMin);
1003 if (this.param.plot[0].type !== "histogram" && this.param.plot[0].type !== "category") {
1004 if (this.param.xAxis.min === undefined)
1006 if (this.param.xAxis.max === undefined)
1008 if (this.param.yAxis.min === undefined)
1011 if (this.param.yAxis.max === undefined)
1025MPlotGraph.prototype.calcStats = function() {
1027 let p = this.param.plot[0];
1029 if (p.type === "scatter") {
1030 this.stats.name = ["Entries", "Mean X", "Std Dev X", "Mean Y", "Std Dev Y"];
1032 this.stats.value = Array(5).fill(0);
1033 let n = this.param.plot[0].xData.length;
1036 let mean = p.xData.reduce((sum, x) => sum + x, 0) / n;
1037 let variance = p.xData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1038 let stddev = Math.sqrt(variance);
1039 this.stats.value[0] = n;
1040 this.stats.value[1] = Number(mean.toPrecision(6));
1041 this.stats.value[2] = Number(stddev.toPrecision(6));
1043 mean = p.yData.reduce((sum, x) => sum + x, 0) / n;
1044 variance = p.yData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1045 stddev = Math.sqrt(variance);
1046 this.stats.value[3] = Number(mean.toPrecision(6));
1047 this.stats.value[4] = Number(stddev.toPrecision(6));
1051 if (p.type === "histogram") {
1052 this.stats.name = ["Entries", "Mean", "Std Dev"];
1054 this.stats.value = Array(3).fill(0);
1055 let n = p.yData.reduce((sum, y) => sum + y, 0);
1062 for (let i=0 ; i< p.xData.length ; i++) {
1064 sumXY += p.xData[i] * p.yData[i];
1065 sumX2Y += p.xData[i] * p.xData[i] * p.yData[i];
1068 let mean = sumXY / sumY;
1069 let variance = sumX2Y / sumY - mean ** 2;
1070 let stddev = Math.sqrt(variance);
1071 this.stats.value[0] = Number(n);
1072 this.stats.value[1] = Number(mean.toPrecision(6));
1073 this.stats.value[2] = Number(stddev.toPrecision(6));
1077 if (p.type === "colormap") {
1078 this.stats.name = ["Entries", "Mean X", "Mean Y", "Std Dev X", "Std Dev Y"];
1080 this.stats.value = Array(5).fill(0);
1082 if (p.nx > 1 && p.ny > 1) {
1085 // calculate x/y values
1086 let dx = (p.xMax - p.xMin) / this.nx;
1087 let dy = (p.yMax - p.yMin) / this.ny;
1089 let xi = Array.from({ length: p.nx }, (_, i) =>
1090 p.xMin + (i + 0.5) * dx);
1092 // sum up all columns projected to X-axis
1093 let sumH = Array(p.nx).fill(0);
1094 for (let i=0 ; i<p.nx ; i++) {
1095 for (let j = 0; j < p.ny; j++) {
1096 n += p.zData[i + j * p.nx];
1097 sumH[i] += p.zData[i + j * p.nx];
1105 for (let i=0 ; i< p.nx ; i++) {
1107 sumXY += xi[i] * sumH[i];
1108 sumX2Y += xi[i] * xi[i] * sumH[i];
1111 let mean = sumXY / sumY;
1112 let variance = sumX2Y / sumY - mean ** 2;
1113 let stddev = Math.sqrt(variance);
1114 this.stats.value[0] = Number(n.toPrecision(6));
1115 this.stats.value[1] = Number(mean.toPrecision(6));
1116 this.stats.value[3] = Number(stddev.toPrecision(6));
1118 //----------------------------------------------
1120 xi = Array.from({ length: p.ny }, (_, i) =>
1121 p.yMin + (i + 0.5) * dy);
1123 // sup up all rows projected to Y-axis
1124 sumH = Array(p.ny).fill(0);
1125 for (let i=0 ; i<p.ny ; i++) {
1126 for (let j = 0; j < p.nx; j++) {
1127 sumH[i] += p.zData[j + i * p.nx];
1135 for (let i=0 ; i< p.ny ; i++) {
1137 sumXY += xi[i] * sumH[i];
1138 sumX2Y += xi[i] * xi[i] * sumH[i];
1141 mean = sumXY / sumY;
1142 variance = sumX2Y / sumY - mean ** 2;
1143 stddev = Math.sqrt(variance);
1144 this.stats.value[2] = Number(mean.toPrecision(6));
1145 this.stats.value[4] = Number(stddev.toPrecision(6));
1151MPlotGraph.prototype.drawMarker = function(ctx, p, x, y) {
1153 * Draw a single marker on plot
1154 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1155 * @param {object} p param object from MPlotGraph
1156 * @param {number} x x coord of marker
1157 * @param {number} y y coord of marker
1159 if (typeof p.marker.lineColor === "string")
1160 ctx.strokeStyle = p.marker.lineColor;
1161 else if (typeof p.marker.lineColor === "number")
1162 ctx.strokeStyle = this.param.color.data[p.marker.lineColor];
1164 if (typeof p.marker.fillColor === "string")
1165 ctx.fillStyle = p.marker.fillColor;
1166 else if (typeof p.marker.fillColor === "number")
1167 ctx.fillStyle = this.param.color.data[p.marker.fillColor];
1169 let size = p.marker.size;
1170 ctx.lineWidth = p.marker.lineWidth;
1172 switch(p.marker.style) {
1175 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
1180 ctx.fillRect(x - size / 2, y - size / 2, size, size);
1181 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
1185 ctx.moveTo(x, y - size / 2);
1186 ctx.lineTo(x + size / 2, y);
1187 ctx.lineTo(x, y + size / 2);
1188 ctx.lineTo(x - size / 2, y);
1189 ctx.lineTo(x, y - size / 2);
1195 ctx.moveTo(x + size * 0.00, y - size * 0.50);
1196 ctx.lineTo(x + size * 0.48, y - size * 0.16);
1197 ctx.lineTo(x + size * 0.30, y + size * 0.41);
1198 ctx.lineTo(x - size * 0.30, y + size * 0.41);
1199 ctx.lineTo(x - size * 0.48, y - size * 0.16);
1200 ctx.lineTo(x + size * 0.00, y - size * 0.50);
1206 ctx.moveTo(x, y - size / 2);
1207 ctx.lineTo(x + size / 2, y + size / 2);
1208 ctx.lineTo(x - size / 2, y + size / 2);
1209 ctx.lineTo(x, y - size / 2);
1213 case "triangle-down":
1215 ctx.moveTo(x, y + size / 2);
1216 ctx.lineTo(x + size / 2, y - size / 2);
1217 ctx.lineTo(x - size / 2, y - size / 2);
1218 ctx.lineTo(x, y + size / 2);
1222 case "triangle-left":
1224 ctx.moveTo(x - size / 2, y);
1225 ctx.lineTo(x + size / 2, y - size / 2);
1226 ctx.lineTo(x + size / 2, y + size / 2);
1227 ctx.lineTo(x - size / 2, y);
1231 case "triangle-right":
1233 ctx.moveTo(x + size / 2, y);
1234 ctx.lineTo(x - size / 2, y - size / 2);
1235 ctx.lineTo(x - size / 2, y + size / 2);
1236 ctx.lineTo(x + size / 2, y);
1242 ctx.moveTo(x - size / 2, y - size / 2);
1243 ctx.lineTo(x + size / 2, y + size / 2);
1244 ctx.moveTo(x - size / 2, y + size / 2);
1245 ctx.lineTo(x + size / 2, y - size / 2);
1250 ctx.moveTo(x - size / 2, y);
1251 ctx.lineTo(x + size / 2, y);
1252 ctx.moveTo(x, y + size / 2);
1253 ctx.lineTo(x, y - size / 2);
1259MPlotGraph.prototype.drawXErrorBar = function(ctx, p, x, y, x1, x2) {
1261 * Draw a single horizontal errorbar on the plot
1262 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1263 * @param {object} p param object from MPlotGraph
1264 * @param {number} x x coord of bar center
1265 * @param {number} y y coord of bar center
1266 * @param {number} x1 low side error bar length
1267 * @param {number} x2 high side error bar length
1270 let size = p.marker.size / 2;
1275 ctx.moveTo(x1, y-size);
1276 ctx.lineTo(x1, y+size);
1277 ctx.moveTo(x2, y-size);
1278 ctx.lineTo(x2, y+size);
1282MPlotGraph.prototype.drawYErrorBar = function(ctx, p, x, y, y1, y2) {
1284 * Draw a single vertical errorbar on the plot
1285 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1286 * @param {object} p param object from MPlotGraph
1287 * @param {number} x x coord of bar center
1288 * @param {number} y y coord of bar center
1289 * @param {number} y1 low side error bar length
1290 * @param {number} y2 high side error bar length
1293 let size = p.marker.size / 2;
1298 ctx.moveTo(x-size, y1);
1299 ctx.lineTo(x+size, y1);
1300 ctx.moveTo(x-size, y2);
1301 ctx.lineTo(x+size, y2);
1305MPlotGraph.prototype.draw = function () {
1314 let ctx = this.canvas.getContext("2d");
1316 this.width = this.canvas.width;
1317 this.height = this.canvas.height;
1319 ctx.fillStyle = this.param.color.background;
1320 ctx.fillRect(0, 0, this.width, this.height);
1322 if (this.error !== null) {
1324 ctx.font = "14px sans-serif";
1325 ctx.strokeStyle = "#808080";
1326 ctx.fillStyle = "#808080";
1327 ctx.textAlign = "center";
1328 ctx.textBaseline = "middle";
1329 ctx.fillText(this.error, this.width / 2, this.height / 2);
1333 if (this.param.plot[0].xData === undefined) {
1335 ctx.font = "14px sans-serif";
1336 ctx.strokeStyle = "#808080";
1337 ctx.fillStyle = "#808080";
1338 ctx.textAlign = "center";
1339 ctx.textBaseline = "middle";
1340 ctx.fillText("No data-odb-path present and no setData() called", this.width / 2, this.height / 2);
1344 if (this.height === undefined || this.width === undefined)
1346 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1349 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1351 let axisLabelWidth = this.drawYAxis(ctx, 50, this.height - 25, this.height - 35,
1352 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.param.yAxis.log, false);
1354 if (axisLabelWidth === undefined)
1357 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1358 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1360 this.x1 = axisLabelWidth + 15;
1362 this.x2 = this.param.showMenuButtons ? this.width - 30 : this.width - 2;
1363 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "")
1364 this.x2 -= 1.0*this.param.zAxis.title.textSize;
1366 if (this.param.showMenuButtons === false)
1367 this.x2 = this.width - 2;
1369 this.y1 = this.height;
1372 let axisLabelHeight;
1373 if (this.param.xAxis.type === "category")
1374 axisLabelHeight = this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1375 10, 12, this.param.plot[0].xData, false);
1377 axisLabelHeight = this.param.xAxis.textSize;
1379 axisLabelHeight += 12; // space for ticks and frame
1381 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "")
1382 this.y1 = this.height - axisLabelHeight - 1.5*this.param.xAxis.title.textSize;
1384 this.y1 = this.height - axisLabelHeight;
1386 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1387 if (this.zMin === undefined || this.zMax === undefined) {
1391 if (this.zMin === this.zMax) {
1396 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1397 axisLabelWidth = this.drawYAxis(ctx, this.x2 + 30, this.y1, this.y1 - this.y2,
1398 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, false);
1399 if (axisLabelWidth === undefined)
1402 if (this.param.zAxis.show) {
1403 let w = 5; // left gap
1404 w += 10; // color bar
1405 w += 12; // tick width
1408 this.x2 -= axisLabelWidth + w;
1409 this.param.zAxis.width = axisLabelWidth + w;
1414 if (this.param.title.text !== "") {
1415 ctx.strokeStyle = this.param.color.axis;
1416 ctx.fillStyle = "#F0F0F0";
1417 ctx.font = this.param.title.textSize + "px sans-serif";
1418 let h = this.param.title.textSize * 1.2;
1419 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, h);
1420 ctx.fillRect(this.x1, 6, this.x2 - this.x1, h);
1421 ctx.textAlign = "center";
1422 ctx.textBaseline = "middle";
1423 ctx.fillStyle = this.param.title.color;
1424 ctx.fillText(this.param.title.text, (this.x2 + this.x1) / 2, 6 + h/2);
1429 ctx.strokeStyle = this.param.color.axis;
1431 if (this.param.yAxis.log && this.yMin < 1E-20)
1433 if (this.param.yAxis.log && this.yMax < 1E-18)
1436 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
1438 ctx.fillStyle = this.param.title.color;
1439 let s = this.param.xAxis.title.textSize;
1440 ctx.font = s + "px sans-serif";
1441 ctx.textAlign = "center";
1442 ctx.textBaseline = "top";
1443 ctx.fillText(this.param.xAxis.title.text, (this.x1 + this.x2)/2,
1444 this.y1 + this.param.xAxis.textSize + 10 + this.param.xAxis.title.textSize / 4);
1448 ctx.font = this.param.xAxis.textSize + "px sans-serif";
1449 let grid = this.param.xAxis.grid ? this.y2 - this.y1 : 0;
1451 if (this.param.xAxis.type === "numeric")
1452 this.drawXAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1453 4, 7, 10, 10, grid, this.xMin, this.xMax, this.param.xAxis.log);
1454 else if (this.param.xAxis.type === "datetime")
1455 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
1456 4, 7, 10, 10, grid, this.xMin, this.xMax);
1457 else if (this.param.xAxis.type === "category")
1458 this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1459 10, 12, this.param.plot[0].xData, true);
1461 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1463 ctx.fillStyle = this.param.title.color;
1464 let s = this.param.yAxis.title.textSize;
1465 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1466 ctx.rotate(-Math.PI / 2);
1467 ctx.font = s + "px sans-serif";
1468 ctx.textAlign = "center";
1469 ctx.textBaseline = "top";
1470 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1474 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
1476 ctx.fillStyle = this.param.title.color;
1477 let s = this.param.zAxis.title.textSize;
1478 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1479 ctx.rotate(-Math.PI / 2);
1480 ctx.font = s + "px sans-serif";
1481 ctx.textAlign = "center";
1482 ctx.textBaseline = "middle";
1483 ctx.fillText(this.param.zAxis.title.text, 0, this.x2 + this.param.zAxis.width);
1487 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1488 grid = this.param.yAxis.grid ? this.x2 - this.x1 : 0;
1489 this.drawYAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
1490 -4, -7, -10, -12, grid, this.yMin, this.yMax, this.param.yAxis.log, true);
1492 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
1494 let s = this.param.yAxis.title.textSize;
1495 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
1496 ctx.rotate(-Math.PI / 2);
1497 ctx.font = s + "px sans-serif";
1498 ctx.textAlign = "center";
1499 ctx.textBaseline = "top";
1500 ctx.fillText(this.param.yAxis.title.text, 0, 0);
1505 ctx.strokeStyle = this.param.color.axis;
1506 ctx.strokeRect(this.x1, this.y1, this.x2-this.x1, this.y2-this.y1);
1508 // set clipping region not to draw outside axes
1510 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
1515 for (const p of this.param.plot) {
1516 if (p.xData === undefined || p.xData === null)
1519 if (p.xData.length > 0)
1522 ctx.globalAlpha = p.alpha;
1524 if (p.type === "scatter") {
1526 if (p.line && p.line.draw ||
1527 p.line && p.line.fill) {
1529 if (typeof p.line.color === "string")
1530 ctx.fillStyle = p.line.color;
1531 else if (typeof p.line.color === "number")
1532 ctx.fillStyle = this.param.color.data[p.line.color];
1533 ctx.strokeStyle = ctx.fillStyle;
1537 ctx.globalAlpha = 0.1;
1539 ctx.moveTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1540 for (let i = 0; i < p.xData.length; i++) {
1541 let x = this.xToScreen(p.xData[i]);
1542 let y = this.yToScreen(p.yData[i]);
1545 ctx.lineTo(this.xToScreen(p.xData[p.xData.length - 1]), this.yToScreen(0));
1546 ctx.lineTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
1548 ctx.globalAlpha = 1;
1552 if (p.line.draw && p.line.width > 0) {
1553 ctx.lineWidth = p.line.width;
1555 for (let i = 0; i < p.xData.length; i++) {
1556 let x = this.xToScreen(p.xData[i]);
1557 let y = this.yToScreen(p.yData[i]);
1568 if (p.marker && p.marker.draw) {
1569 for (let i = 0; i < p.xData.length; i++) {
1571 let x = this.xToScreen(p.xData[i]);
1572 let y = this.yToScreen(p.yData[i]);
1574 this.drawMarker(ctx, p, x, y);
1577 let x1 = this.xToScreen(p.xData[i]-p.xErrorData[i]);
1578 let x2 = this.xToScreen(p.xData[i]+p.xErrorData[i]);
1579 this.drawXErrorBar(ctx, p, x, y, x1, x2);
1583 let y1 = this.yToScreen(p.yData[i]+p.yErrorData[i]);
1584 let y2 = this.yToScreen(p.yData[i]-p.yErrorData[i]);
1585 this.drawYErrorBar(ctx, p, x, y, y1, y2);
1591 else if (p.type === "histogram") {
1593 let dx = (p.xMax - p.xMin) / p.xData.length;
1594 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1601 if (typeof p.line.color === "string")
1602 ctx.fillStyle = p.line.color;
1603 else if (typeof p.line.color === "number")
1604 ctx.fillStyle = this.param.color.data[p.line.color];
1605 ctx.strokeStyle = ctx.fillStyle;
1608 ctx.moveTo(this.xToScreen(p.xData[0])-dxs/2, this.yToScreen(0));
1609 for (let i = 0; i < p.xData.length; i++) {
1610 x = this.xToScreen(p.xData[i]);
1611 y = this.yToScreen(p.yData[i]);
1612 ctx.lineTo(x-dxs/2, y);
1613 ctx.lineTo(x+dxs/2, y);
1615 ctx.lineTo(x+dxs/2, this.yToScreen(0));
1616 ctx.globalAlpha = 0.2;
1618 ctx.globalAlpha = 1;
1622 else if (p.type === "category") {
1624 let dx = (this.x2 - this. x1) / p.xData.length;
1628 width = dx * p.barWidth;
1632 if (p.xData.length < 100)
1637 if (typeof p.line.color === "string")
1638 ctx.fillStyle = p.line.color;
1639 else if (typeof p.line.color === "number")
1640 ctx.fillStyle = this.param.color.data[p.line.color];
1641 ctx.strokeStyle = ctx.fillStyle;
1644 for (let i = 0; i < p.xData.length; i++) {
1645 x = this.xToScreen(i + 0.5);
1646 y = this.yToScreen(p.yData[i]);
1647 ctx.moveTo(x-width/2, this.yToScreen(0));
1648 ctx.lineTo(x-width/2, y);
1649 ctx.lineTo(x+width/2, y);
1650 ctx.lineTo(x+width/2, this.yToScreen(0));
1651 ctx.lineTo(x-width/2, this.yToScreen(0));
1653 ctx.globalAlpha = 0.2;
1655 ctx.globalAlpha = 1;
1659 else if (p.type === "colormap") {
1660 let dx = (p.xMax - p.xMin) / this.nx;
1661 let dy = (p.yMax - p.yMin) / this.ny;
1663 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
1664 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
1666 for (let i=0 ; i<p.ny ; i++) {
1667 for (let j=0 ; j<p.nx ; j++) {
1668 let x = this.xToScreen(j * dx + p.xMin);
1669 let y = this.yToScreen(i * dy + p.yMin);
1670 let zval = this.param.plot[0].zData[j+i*p.nx];
1672 ctx.fillStyle = 'hsl(255, 0%, 50%)';
1675 if (this.param.zAxis.log) {
1679 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
1681 v = (zval - this.zMin) / (this.zMax - this.zMin);
1689 if (zval < 0.5 && this.param.plot[0].bgcolor)
1690 ctx.fillStyle = this.param.plot[0].bgcolor;
1692 ctx.fillStyle = 'hsl(' + Math.floor((1 - v) * 240) + ', 100%, 50%)';
1694 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
1701 ctx.restore(); // remove clipping
1704 if (this.param.plot[0].type === "colormap") {
1705 if (this.param.plot[0].showZScale) {
1707 for (let i=0 ; i<100 ; i++) {
1709 ctx.fillStyle = 'hsl(' +
1710 Math.floor(v * 240) + ', 100%, 50%)';
1711 ctx.fillRect(this.x2 + 5, this.y2 + i/100*(this.y1 - this.y2),
1712 10, (this.y1 - this.y2) / 100 + 1);
1716 ctx.strokeStyle = this.param.color.axis;
1718 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
1721 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1722 ctx.strokeStyle = this.param.color.axis;
1724 this.drawYAxis(ctx, this.x2 + 15, this.y1, this.y1 - this.y2,
1725 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, true);
1731 for (const p of this.param.plot)
1732 if (p.label && p.label !== "")
1735 if (this.param.legend?.show && nLabel > 0) {
1736 ctx.font = this.param.legend.textSize + "px sans-serif";
1739 for (const p of this.param.plot) {
1740 if (ctx.measureText(p.label).width > mw) {
1741 mw = ctx.measureText(p.label).width;
1744 let w = 50 + mw + 5;
1745 let h = this.param.legend.textSize * 1.5;
1747 ctx.fillStyle = this.param.legend.backgroundColor;
1748 ctx.strokeStyle = this.param.legend.color;
1749 ctx.fillRect(this.x1, this.y2, w, h * this.param.plot.length);
1750 ctx.strokeRect(this.x1, this.y2, w, h * this.param.plot.length);
1752 for (const [pi,p] of this.param.plot.entries()) {
1753 if (p.line && p.line.draw && p.line.width > 0) {
1755 ctx.strokeStyle = this.param.color.data[p.line.color];
1756 ctx.lineWidth = p.line.width;
1758 ctx.moveTo(this.x1 + 5, this.y2 + pi*h + h/2);
1759 ctx.lineTo(this.x1 + 35, this.y2 + pi*h + h/2);
1763 this.drawMarker(ctx, p, this.x1 + 20, this.y2 + pi*h + h/2);
1765 ctx.textAlign = "left";
1766 ctx.textBaseline = "middle";
1767 ctx.fillStyle = this.param.color.axis;
1768 ctx.fillText(p.label, this.x1 + 40, this.y2 + pi*h + h/2);
1775 if (this.param.stats.show && this.stats.name) {
1776 ctx.font = this.param.legend.textSize + "px sans-serif";
1779 for (const [si,s] of this.stats.name.entries()) {
1780 let str = s + " " + this.stats.value[si].toString();
1781 if (ctx.measureText(str).width > mw) {
1782 mw = ctx.measureText(str).width;
1786 let h = this.param.legend.textSize * 1.5;
1788 ctx.fillStyle = this.param.legend.backgroundColor;
1789 ctx.strokeStyle = this.param.legend.color;
1790 ctx.fillRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
1791 ctx.strokeRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
1793 for (const [si,s] of this.stats.name.entries()) {
1794 ctx.textAlign = "left";
1795 ctx.textBaseline = "middle";
1796 ctx.fillStyle = this.param.color.axis;
1797 ctx.fillText(s, this.x2 - w + 5, this.y2 + si*h + h/2);
1798 ctx.textAlign = "right";
1799 let str = this.stats.value[si].toString();
1800 ctx.fillText(str, this.x2 - 5, this.y2 + si*h + h/2);
1804 // "empty window" notice
1806 ctx.font = "16px sans-serif";
1807 let str = "No data available";
1808 ctx.strokeStyle = "#404040";
1809 ctx.fillStyle = "#F0F0F0";
1810 let w = ctx.measureText(str).width + 10;
1812 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1813 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
1814 ctx.fillStyle = "#404040";
1815 ctx.textAlign = "center";
1816 ctx.textBaseline = "middle";
1817 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
1818 ctx.font = "14px sans-serif";
1822 if (this.param.showMenuButtons) {
1824 let buttonSize = 20;
1825 this.button.forEach(b => {
1827 if (!(this.param.plot[0].type === "category" && b.src === "stats.svg")) {
1828 b.x1 = this.width - buttonSize - 6;
1829 b.y1 = 6 + y * (buttonSize + 4);
1830 b.width = buttonSize + 4;
1831 b.height = buttonSize + 4;
1834 ctx.fillStyle = "#F0F0F0";
1835 ctx.strokeStyle = "#808080";
1836 ctx.fillRect(b.x1, b.y1, b.width, b.height);
1837 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
1838 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
1846 if (this.zoom.x.active) {
1847 ctx.fillStyle = "#808080";
1848 ctx.globalAlpha = 0.2;
1849 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
1850 ctx.globalAlpha = 1;
1851 ctx.strokeStyle = "#808080";
1852 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
1853 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
1855 if (this.zoom.y.active) {
1856 ctx.fillStyle = "#808080";
1857 ctx.globalAlpha = 0.2;
1858 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
1859 ctx.globalAlpha = 1;
1860 ctx.strokeStyle = "#808080";
1861 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
1862 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
1866 if (this.marker.active) {
1869 if (this.param.plot[0].type !== "colormap") {
1871 ctx.globalAlpha = 0.1;
1872 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
1873 ctx.fillStyle = "#000000";
1875 ctx.globalAlpha = 1;
1878 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
1879 ctx.fillStyle = "#000000";
1883 ctx.strokeStyle = "#A0A0A0";
1884 ctx.drawLine(this.marker.sx, this.y1, this.marker.sx, this.y2);
1885 ctx.drawLine(this.x1, this.marker.sy, this.x2, this.marker.sy);
1888 ctx.font = "12px sans-serif";
1889 ctx.textAlign = "left";
1891 if (this.parentDiv.dataset.tooltip) {
1892 let f = this.parentDiv.dataset.tooltip;
1893 if (f.indexOf('(') !== -1) // strip any '('
1894 f = f.substring(0, f.indexOf('('));
1896 s = eval(f + "(this)");
1898 s = this.marker.x.toPrecision(6).stripZeros() + " / " +
1899 this.marker.y.toPrecision(6).stripZeros();
1900 if (this.param.plot[0].type === "colormap")
1901 s += ": " + (this.marker.z === null ? "null" : this.marker.z.toPrecision(6).stripZeros());
1903 let w = ctx.measureText(s).width + 6;
1904 let h = ctx.measureText("M").width * 1.2 + 6;
1905 let x = this.marker.mx + 10;
1906 let y = this.marker.my - 20;
1908 // move marker inside if outside plotting area
1909 if (x + w >= this.x2)
1910 x = this.marker.sx - 10 - w;
1912 ctx.strokeStyle = "#808080";
1913 ctx.fillStyle = "#F0F0F0";
1914 ctx.textBaseline = "middle";
1915 ctx.fillRect(x, y, w, h);
1916 ctx.strokeRect(x, y, w, h);
1917 ctx.fillStyle = "#404040";
1918 ctx.fillText(s, x + 3, y + h / 2);
1921 // call optional user overlay function
1922 if (this.param.overlay) {
1925 ctx.textAlign = "left";
1926 ctx.textBaseline = "top";
1927 ctx.fillStyle = "black";
1928 ctx.strokeStyle = "black";
1929 ctx.font = "12px sans-serif";
1931 eval(this.param.overlay + "(this, ctx)");
1941MPlotGraph.prototype.drawXAxis = function (ctx, x1, y1, width, minor, major,
1942 text, label, grid, xmin, xmax, logaxis) {
1944 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1945 * @param {number} x1 coordinate position of the axis on screen
1946 * @param {number} y1 coordinate position of the axis on screen
1947 * @param {number} width width of the axis, likely also the width of the plot
1948 * @param {number} minor step between minor ticks
1949 * @param {number} major step between major ticks
1950 * @param {string} text
1951 * @param {string} label
1952 * @param {bool} grid if true draw grid lines over minor ticks
1953 * @param {number} ymin low limit of the axis
1954 * @param {number} ymax high limit of the axis
1955 * @param {bool} logaxis if true draw axis on a log scale (base 10)
1957 var dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1958 var tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1959 var base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1961 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
1964 if (xmax <= xmin || width <= 0)
1967 ctx.textAlign = "center";
1968 ctx.textBaseline = "top";
1972 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1973 if (isNaN(dx) || dx === 0) {
1981 } else { // linear axis ----
1983 // use 10 as min tick distance
1984 dx = (xmax - xmin) / (width / 10);
1986 int_dx = Math.floor(Math.log(dx) / LN10);
1987 frac_dx = Math.log(dx) / LN10 - int_dx;
1994 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
1995 major_base = label_base = tick_base + 1;
1997 // rounding up of dx, label_dx
1998 dx = Math.pow(10, int_dx) * base[tick_base];
1999 major_dx = Math.pow(10, int_dx) * base[major_base];
2000 label_dx = major_dx;
2003 // number of significant digits
2007 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2012 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2014 n_sig1 = Math.max(n_sig1, n_sig2);
2016 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2017 if (Math.abs(xmin) < 100000)
2018 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / LN10) + 1);
2019 if (Math.abs(xmax) < 100000)
2020 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / LN10) + 1);
2022 // determination of maximal width of labels
2023 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
2024 let ext = ctx.measureText(str);
2025 maxwidth = ext.width;
2027 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
2028 ext = ctx.measureText(str);
2029 maxwidth = Math.max(maxwidth, ext.width);
2030 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
2031 maxwidth = Math.max(maxwidth, ext.width);
2033 // increasing label_dx, if labels would overlap
2034 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
2036 label_dx = Math.pow(10, int_dx) * base[label_base];
2037 if (label_base % 3 === 2 && major_base % 3 === 1) {
2039 major_dx = Math.pow(10, int_dx) * base[major_base];
2047 x_act = Math.floor(xmin / dx) * dx;
2049 ctx.strokeStyle = this.param.color.axis;
2050 ctx.drawLine(x1, y1, x1 + width, y1);
2054 x_screen = (Math.log(x_act) - Math.log(xmin)) /
2055 (Math.log(xmax) - Math.log(xmin)) * width + x1;
2057 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
2058 xs = Math.floor(x_screen + 0.5);
2060 if (x_screen > x1 + width + 0.001)
2063 if (x_screen >= x1) {
2064 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
2065 dx / major_dx / 10.0) {
2067 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
2068 dx / label_dx / 10.0) {
2070 ctx.strokeStyle = this.param.color.axis;
2071 ctx.drawLine(xs, y1, xs, y1 + text);
2074 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2075 ctx.strokeStyle = this.param.color.grid;
2076 ctx.drawLine(xs, y1, xs, y1 + grid);
2081 str = x_act.toPrecision(n_sig1).stripZeros();
2082 ext = ctx.measureText(str);
2083 if (xs - ext.width / 2 > x1 &&
2084 xs + ext.width / 2 < x1 + width) {
2085 ctx.strokeStyle = this.param.color.label;
2086 ctx.fillStyle = this.param.color.label;
2087 ctx.fillText(str, xs, y1 + label);
2089 last_label_x = xs + ext.width / 2;
2093 ctx.strokeStyle = this.param.color.axis;
2094 ctx.drawLine(xs, y1, xs, y1 + major);
2097 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2098 ctx.strokeStyle = this.param.color.grid;
2099 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2111 ctx.strokeStyle = this.param.color.axis;
2112 ctx.drawLine(xs, y1, xs, y1 + minor);
2116 // for log axis, also put grid lines on minor tick marks
2117 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2118 ctx.strokeStyle = this.param.color.grid;
2119 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2122 // for log axis, also put labels on minor tick marks
2125 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
2126 str = x_act.toExponential(n_sig1).stripZeros();
2128 str = x_act.toPrecision(n_sig1).stripZeros();
2129 ext = ctx.measureText(str);
2130 if (xs - ext.width / 2 > x1 &&
2131 xs + ext.width / 2 < x1 + width &&
2132 xs - ext.width / 2 > last_label_x + 5) {
2133 ctx.strokeStyle = this.param.color.label;
2134 ctx.fillStyle = this.param.color.label;
2135 ctx.fillText(str, xs, y1 + label);
2138 last_label_x = xs + ext.width / 2;
2145 /* suppress 1.23E-17 ... */
2146 if (Math.abs(x_act) < dx / 100)
2152MPlotGraph.prototype.drawYAxis = function (ctx, x1, y1, height, minor, major,
2153 text, label, grid, ymin, ymax, logaxis, draw) {
2155 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2156 * @param {number} x1 coordinate position of the axis on screen
2157 * @param {number} y1 coordinate position of the axis on screen
2158 * @param {number} height height of the axis, likely also the height of the plot
2159 * @param {number} minor step between minor ticks
2160 * @param {number} major step between major ticks
2161 * @param {string} text
2162 * @param {string} label
2163 * @param {bool} grid if true draw grid lines over minor ticks
2164 * @param {number} ymin low limit of the axis
2165 * @param {number} ymax high limit of the axis
2166 * @param {bool} logaxis if true draw axis on a log scale (base 10)
2167 * @param {bool} draw if true draw the axis
2169 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
2170 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
2171 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
2173 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
2176 if (ymax <= ymin || height <= 0)
2180 ctx.textAlign = "right";
2182 ctx.textAlign = "left";
2183 ctx.textBaseline = "middle";
2184 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
2186 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
2187 dy = Number.MAX_VALUE / 10;
2191 } else if (logaxis) {
2192 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
2193 if (isNaN(dy) || dy === 0) {
2201 // use 6 as min tick distance
2202 dy = (ymax - ymin) / (height / 6);
2204 int_dy = Math.floor(Math.log(dy) / Math.log(10));
2205 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
2212 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
2213 major_base = label_base = tick_base + 1;
2215 // rounding up of dy, label_dy
2216 dy = Math.pow(10, int_dy) * base[tick_base];
2217 major_dy = Math.pow(10, int_dy) * base[major_base];
2218 label_dy = major_dy;
2220 // number of significant digits
2224 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
2225 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2230 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
2231 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2233 n_sig1 = Math.max(n_sig1, n_sig2);
2234 n_sig1 = Math.max(1, n_sig1);
2236 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2237 if (Math.abs(ymin) < 100000)
2238 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
2239 Math.log(10) + 0.001) + 1);
2240 if (Math.abs(ymax) < 100000)
2241 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
2242 Math.log(10) + 0.001) + 1);
2244 // increase label_dy if labels would overlap
2245 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
2247 label_dy = Math.pow(10, int_dy) * base[label_base];
2248 if (label_base % 3 === 2 && major_base % 3 === 1) {
2250 major_dy = Math.pow(10, int_dy) * base[major_base];
2255 y_act = Math.floor(ymin / dy) * dy;
2257 let last_label_y = y1;
2261 ctx.strokeStyle = this.param.color.axis;
2262 ctx.drawLine(x1, y1, x1, y1 - height);
2267 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
2268 (Math.log(ymax) - Math.log(ymin)) * height;
2269 else if (!(isFinite(ymax - ymin)))
2270 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
2272 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
2273 ys = Math.round(y_screen);
2275 if (y_screen < y1 - height - 0.001 || isNaN(ys))
2278 if (y_screen <= y1 + 0.001) {
2279 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
2280 dy / major_dy / 10.0) {
2282 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
2283 dy / label_dy / 10.0) {
2286 ctx.strokeStyle = this.param.color.axis;
2287 ctx.drawLine(x1, ys, x1 + text, ys);
2291 if (grid !== 0 && ys < y1 && ys > y1 - height)
2293 ctx.strokeStyle = this.param.color.grid;
2294 ctx.drawLine(x1, ys, x1 + grid, ys);
2300 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2301 str = y_act.toExponential(n_sig1).stripZeros();
2303 str = y_act.toPrecision(n_sig1).stripZeros();
2304 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2306 ctx.strokeStyle = this.param.color.label;
2307 ctx.fillStyle = this.param.color.label;
2308 ctx.fillText(str, x1 + label, ys);
2310 last_label_y = ys - textHeight / 2;
2315 ctx.strokeStyle = this.param.color.axis;
2316 ctx.drawLine(x1, ys, x1 + major, ys);
2320 if (grid !== 0 && ys < y1 && ys > y1 - height)
2322 ctx.strokeStyle = this.param.color.grid;
2323 ctx.drawLine(x1, ys, x1 + grid, ys);
2336 ctx.strokeStyle = this.param.color.axis;
2337 ctx.drawLine(x1, ys, x1 + minor, ys);
2343 // for log axis, also put grid lines on minor tick marks
2344 if (grid !== 0 && ys < y1 && ys > y1 - height) {
2346 ctx.strokeStyle = this.param.color.grid;
2347 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
2351 // for log axis, also put labels on minor tick marks
2354 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2355 str = y_act.toExponential(n_sig1).stripZeros();
2357 str = y_act.toPrecision(n_sig1).stripZeros();
2358 if (ys - textHeight / 2 > y1 - height &&
2359 ys + textHeight / 2 < y1 &&
2360 ys + textHeight < last_label_y + 2) {
2361 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2363 ctx.strokeStyle = this.param.color.label;
2364 ctx.fillStyle = this.param.color.label;
2365 ctx.fillText(str, x1 + label, ys);
2376 // suppress 1.23E-17 ...
2377 if (Math.abs(y_act) < dy / 100)
2385/* Begin timeToLabel format options */
2388 day: '2-digit', month: 'short', year: '2-digit',
2389 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
2394 day: '2-digit', month: 'short', year: '2-digit',
2395 hour12: false, hour: '2-digit', minute: '2-digit'
2400 day: '2-digit', month: 'short', year: '2-digit',
2401 hour12: false, hour: '2-digit', minute: '2-digit'
2406 day: '2-digit', month: 'short', year: '2-digit'
2411 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
2416 hour12: false, hour: '2-digit', minute: '2-digit'
2421 hour12: false, hour: '2-digit', minute: '2-digit'
2426 day: '2-digit', month: 'short', year: '2-digit',
2427 hour12: false, hour: '2-digit', minute: '2-digit'
2432 day: '2-digit', month: 'short', year: '2-digit'
2434/* End timeToLabel format options */
2436function timeToLabel(sec, base, forceDate) {
2438 * Convert time in seconds to a human-readable string
2439 * @param {number} sec number of seconds
2440 * @param {number} base chooses which display option to use on conversion
2441 * @param {bool} forceDate if true force showing the date, else can show only time
2442 * @returns {string} human-readable datetime as a string
2444 let d = mhttpd_get_display_time(sec).date;
2448 return d.toLocaleTimeString('en-GB', options1);
2449 } else if (base < 600) {
2450 return d.toLocaleTimeString('en-GB', options2);
2451 } else if (base < 3600 * 24) {
2452 return d.toLocaleTimeString('en-GB', options3);
2454 return d.toLocaleDateString('en-GB', options4);
2459 return d.toLocaleTimeString('en-GB', options5);
2460 } else if (base < 600) {
2461 return d.toLocaleTimeString('en-GB', options6);
2462 } else if (base < 3600 * 3) {
2463 return d.toLocaleTimeString('en-GB', options7);
2464 } else if (base < 3600 * 24) {
2465 return d.toLocaleTimeString('en-GB', options8);
2467 return d.toLocaleDateString('en-GB', options9);
2471MPlotGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
2472 text, label, grid, xmin, xmax) {
2473 /** Draw the xaxis as time
2474 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2475 * @param {number} x1 coordinate position of the axis on screen
2476 * @param {number} y1 coordinate position of the axis on screen
2477 * @param {number} width width of the axis, likely also the width of the plot
2478 * @param {number} xr
2479 * @param {number} minor step between minor ticks
2480 * @param {number} major step between major ticks
2481 * @param {string} text
2482 * @param {string} label
2483 * @param {bool} grid if true draw grid lines over minor ticks
2484 * @param {number} xmin low limit of the axis
2485 * @param {number} xmax high limit of the axis
2487 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
2488 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
2490 ctx.textAlign = "left";
2491 ctx.textBaseline = "top";
2493 if (xmax <= xmin || width <= 0)
2496 /* force date display if xmax not today */
2497 let d1 = new Date(xmax * 1000);
2498 let d2 = new Date();
2499 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
2501 /* use 5 pixel as min tick distance */
2502 let dx = Math.round((xmax - xmin) / (width / 5));
2505 for (tick_base = 0; base[tick_base]; tick_base++) {
2506 if (base[tick_base] > dx)
2509 if (!base[tick_base])
2511 dx = base[tick_base];
2513 let major_base = tick_base;
2516 let label_base = major_base;
2520 let str = timeToLabel(xmin, label_dx, forceDate);
2521 let maxWidth = ctx.measureText(str).width;
2523 /* increasing label_dx, if labels would overlap */
2524 if (maxWidth > 0.75 * label_dx / (xmax - xmin) * width) {
2525 if (base[label_base + 1])
2526 label_dx = base[++label_base];
2528 label_dx += 3600 * 24;
2530 if (label_base > major_base + 1 || !base[label_base + 1]) {
2531 if (base[major_base + 1])
2532 major_dx = base[++major_base];
2534 major_dx += 3600 * 24;
2537 if (major_base > tick_base + 1 || !base[label_base + 1]) {
2538 if (base[tick_base + 1])
2539 dx = base[++tick_base];
2548 let d = new Date(xmin * 1000);
2549 let tz = d.getTimezoneOffset() * 60;
2551 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
2553 ctx.strokeStyle = this.param.color.axis;
2554 ctx.drawLine(x1, y1, x1 + width, y1);
2557 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
2559 if (xs > x1 + width + 0.001)
2563 if ((x_act - tz) % major_dx === 0) {
2564 if ((x_act - tz) % label_dx === 0) {
2566 ctx.strokeStyle = this.param.color.axis;
2567 ctx.drawLine(xs, y1, xs, y1 + text);
2570 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2571 ctx.strokeStyle = this.param.color.grid;
2572 ctx.drawLine(xs, y1, xs, y1 + grid);
2577 let str = timeToLabel(x_act, label_dx, forceDate);
2579 // if labels at edge, shift them in
2580 let xl = xs - ctx.measureText(str).width / 2;
2583 if (xl + ctx.measureText(str).width >= xr)
2584 xl = xr - ctx.measureText(str).width - 1;
2585 ctx.strokeStyle = this.param.color.label;
2586 ctx.fillStyle = this.param.color.label;
2587 ctx.fillText(str, xl, y1 + label);
2591 ctx.strokeStyle = this.param.color.axis;
2592 ctx.drawLine(xs, y1, xs, y1 + major);
2596 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2597 ctx.strokeStyle = this.param.color.grid;
2598 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2602 ctx.strokeStyle = this.param.color.axis;
2603 ctx.drawLine(xs, y1, xs, y1 + minor);
2612MPlotGraph.prototype.drawCAxis = function (ctx, x1, y1, width, tick, label, category, draw) {
2613 /** Draw the xaxis as time
2614 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2615 * @param {number} x1 coordinate position of the axis on screen
2616 * @param {number} y1 coordinate position of the axis on screen
2617 * @param {number} width width of the axis, likely also the width of the plot
2618 * @param {number} tick height in pixel of tick markers at labels
2619 * @param {number} label distance of text from axis
2620 * @param {string} category array of category labels to plot
2621 * @param {bool} draw if false, only return height of labels
2623 ctx.textAlign = "center";
2624 ctx.textBaseline = "middle";
2629 ctx.strokeStyle = this.param.color.axis;
2632 ctx.drawLine(x1, y1, x1 + width, y1);
2634 let dx = width/category.length;
2639 for (angle = 0 ; angle < 90 ; angle += 10) {
2642 for (let i = 0; i < category.length; i++) {
2646 ctx.drawLine(x1 + dx * (i + 0.5), y1, x1 + dx * (i + 0.5), y1 + tick);
2649 let w = ctx.measureText(category[i]).width;
2650 let h = this.param.xAxis.textSize;
2652 const cos = Math.cos(angle/180*Math.PI);
2653 const sin = Math.sin(angle/180*Math.PI);
2655 const width = Math.abs(w * cos) + Math.abs(h * sin);
2656 const height = Math.abs(w * sin) + Math.abs(h * cos);
2658 maxWidth = Math.max(maxWidth, width);
2659 maxHeight = Math.max(maxHeight, height);
2662 if (maxWidth * 1.1 < dx)
2666 this.param.xAxis.angle = angle;
2668 for (let i = 0; i < category.length; i++) {
2670 ctx.translate(x1 + dx * (i + 0.5), y1 + label + maxHeight / 2);
2671 ctx.rotate(-angle / 180 * Math.PI);
2672 ctx.fillText(category[i], 0, 0);
2677 return maxHeight + 2;
2680MPlotGraph.prototype.download = function (mode) {
2682 * Download the figure as an image or data
2683 * @param {string} mode either "CSV" | "PNG"
2687 let filename = this.param.title.text + "-" +
2689 ("0" + (d.getUTCMonth() + 1)).slice(-2) +
2690 ("0" + d.getUTCDate()).slice(-2) + "-" +
2691 ("0" + d.getUTCHours()).slice(-2) +
2692 ("0" + d.getUTCMinutes()).slice(-2) +
2693 ("0" + d.getUTCSeconds()).slice(-2);
2695 // use trick from FileSaver.js
2696 let a = document.getElementById('downloadHook');
2698 a = document.createElement("a");
2699 a.style.display = "none";
2700 a.id = "downloadHook";
2701 document.body.appendChild(a);
2704 if (mode === "CSV") {
2710 this.param.plot.forEach(p => {
2711 if (p.type === "scatter" || p.type === "histogram") {
2720 for (let i = 0; i < p.xData.length; i++) {
2721 data += p.xData[i] + ",";
2722 data += p.yData[i] + "\n";
2727 if (p.type === "colormap") {
2731 for (let i = 0; i < p.nx; i++)
2732 data += p.xData[i] + ",";
2735 for (let j = 0; j < p.ny; j++) {
2736 data += p.yData[j] + ",";
2737 for (let i = 0; i < p.nx; i++)
2738 data += p.zData[i + j * p.nx] + ",";
2744 let blob = new Blob([data], {type: "text/csv"});
2745 let url = window.URL.createObjectURL(blob);
2748 a.download = filename;
2750 window.URL.revokeObjectURL(url);
2751 dlgAlert("Data downloaded to '" + filename + "'");
2753 } else if (mode === "PNG") {
2756 let smb = this.param.showMenuButtons;
2757 this.param.showMenuButtons = false;
2761 this.canvas.toBlob(function (blob) {
2762 let url = window.URL.createObjectURL(blob);
2765 a.download = filename;
2767 window.URL.revokeObjectURL(url);
2768 dlgAlert("Image downloaded to '" + filename + "'");
2770 h.param.showMenuButtons = smb;
2778MPlotGraph.prototype.drawTextBox = function (ctx, text, x, y) {
2780 * Draw a box encapsulating some text. Width and height are set by the text
2781 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2782 * @param {string} text text to include in the box
2783 * @param {number} x coordinate of box lower left corner
2784 * @param {number} y coordinate of box lower left corner
2787 let line = text.split("\n");
2790 for (const p of line)
2791 if (ctx.measureText(p).width > mw)
2792 mw = ctx.measureText(p).width;
2794 let h = parseInt(ctx.font) * 1.5;
2796 let c = ctx.fillStyle;
2797 ctx.fillStyle = "white";
2798 ctx.fillRect(x, y, w, h * line.length);
2800 ctx.strokeRect(x, y, w, h * line.length);
2802 for (let i=0 ; i<line.length ; i++)
2803 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
2806MPlotGraph.prototype.mouseEvent = function (e) {
2808 * Handle mouse events
2809 * @param {Object} e mouse event object, specifies type, buttons
2812 // execute callback if registered
2813 if (this.param.event) {
2815 if (this.param.plot[0].type === "colormap") {
2816 // pass plot column/row to callback
2817 let x = this.screenToX(e.offsetX);
2818 let y = this.screenToY(e.offsetY);
2819 let xMin = this.param.plot[0].xMin;
2820 let xMax = this.param.plot[0].xMax;
2821 let yMin = this.param.plot[0].yMin;
2822 let yMax = this.param.plot[0].yMax;
2823 let dx = (xMax - xMin) / this.nx;
2824 let dy = (yMax - yMin) / this.ny;
2825 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
2826 x > xMin && x < xMax && y > yMin && y < yMax) {
2827 let ix = Math.floor((x - xMin) / dx);
2828 let iy = Math.floor((y - yMin) / dy);
2830 let flag = eval(this.param.event + "(e, this, ix, iy)");
2836 // call all other plots only with event and object
2837 let flag = eval(this.param.event + "(e, this)");
2844 // fix buttons for IE
2845 if (!e.which && e.button) {
2846 if ((e.button & 1) > 0) e.which = 1; // Left
2847 else if ((e.button & 4) > 0) e.which = 2; // Middle
2848 else if ((e.button & 2) > 0) e.which = 3; // Right
2851 let cursor = "default";
2855 // cancel dragging in case we did not catch the mouseup event
2856 if (e.type === "mousemove" && e.buttons === 0 &&
2857 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
2860 if (e.type === "mousedown") {
2862 this.downloadSelector.style.display = "none";
2864 // check for buttons
2865 this.button.forEach(b => {
2866 if (e.offsetX > b.x1 && e.offsetX < b.x1 + b.width &&
2867 e.offsetY > b.y1 && e.offsetY < b.y1 + b.width &&
2873 // check for dragging
2874 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
2875 e.offsetY > this.y2 && e.offsetY < this.y1) {
2876 this.drag.active = true;
2877 this.marker.active = false;
2878 this.drag.sxStart = e.offsetX;
2879 this.drag.syStart = e.offsetY;
2880 this.drag.xStart = this.screenToX(e.offsetX);
2881 this.drag.yStart = this.screenToY(e.offsetY);
2882 this.drag.xMinStart = this.xMin;
2883 this.drag.xMaxStart = this.xMax;
2884 this.drag.yMinStart = this.yMin;
2885 this.drag.yMaxStart = this.yMax;
2887 this.blockAutoScale = true;
2890 // check for axis dragging
2891 if (e.offsetX > this.x1 && e.offsetX < this.x2 && e.offsetY > this.y1) {
2892 this.zoom.x.active = true;
2893 this.zoom.x.x1 = e.offsetX;
2894 this.zoom.x.x2 = undefined;
2895 this.zoom.x.t1 = this.screenToX(e.offsetX);
2897 if (e.offsetY < this.y1 && e.offsetY > this.y2 && e.offsetX < this.x1) {
2898 this.zoom.y.active = true;
2899 this.zoom.y.y1 = e.offsetY;
2900 this.zoom.y.y2 = undefined;
2901 this.zoom.y.v1 = this.screenToY(e.offsetY);
2904 } else if (cancel || e.type === "mouseup") {
2906 if (this.drag.active)
2907 this.drag.active = false;
2909 if (this.zoom.x.active) {
2910 if (this.zoom.x.x2 !== undefined &&
2911 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
2912 let x1 = this.zoom.x.t1;
2913 let x2 = this.screenToX(this.zoom.x.x2);
2915 [x1, x2] = [x2, x1];
2919 this.zoom.x.active = false;
2920 this.blockAutoScale = true;
2924 if (this.zoom.y.active) {
2925 if (this.zoom.y.y2 !== undefined &&
2926 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
2927 let y1 = this.zoom.y.v1;
2928 let y2 = this.screenToY(this.zoom.y.y2);
2930 [y1, y2] = [y2, y1];
2934 this.zoom.y.active = false;
2935 this.blockAutoScale = true;
2939 } else if (e.type === "mousemove") {
2941 if (this.drag.active) {
2946 if (this.param.xAxis.log) {
2947 let dx = e.offsetX - this.drag.sxStart;
2949 this.xMin = Math.exp(((this.x1 - dx) - this.x1) / (this.x2 - this.x1) * (Math.log(this.drag.xMaxStart)-Math.log(this.drag.xMinStart)) + Math.log(this.drag.xMinStart));
2950 this.xMax = Math.exp(((this.x2 - dx) - this.x1) / (this.x2 - this.x1) * (Math.log(this.drag.xMaxStart)-Math.log(this.drag.xMinStart)) + Math.log(this.drag.xMinStart));
2957 let dx = (e.offsetX - this.drag.sxStart) / (this.x2 - this.x1) * (this.xMax - this.xMin);
2958 this.xMin = this.drag.xMinStart - dx;
2959 this.xMax = this.drag.xMaxStart - dx;
2962 if (this.param.yAxis.log) {
2963 let dy = e.offsetY - this.drag.syStart;
2965 this.yMin = Math.exp((this.y1 - (this.y1 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
2966 this.yMax = Math.exp((this.y1 - (this.y2 - dy)) / (this.y1 - this.y2) * (Math.log(this.drag.yMaxStart)-Math.log(this.drag.yMinStart)) + Math.log(this.drag.yMinStart));
2973 let dy = (this.drag.syStart - e.offsetY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
2974 this.yMin = this.drag.yMinStart - dy;
2975 this.yMax = this.drag.yMaxStart - dy;
2982 // change cursor to pointer over buttons
2983 this.button.forEach(b => {
2984 if (e.offsetX > b.x1 && e.offsetY > b.y1 &&
2985 e.offsetX < b.x1 + b.width && e.offsetY < b.y1 + b.height) {
2991 // execute axis zoom
2992 if (this.zoom.x.active) {
2993 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, e.offsetX));
2994 this.zoom.x.t2 = this.screenToX(e.offsetX);
2997 if (this.zoom.y.active) {
2998 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, e.offsetY));
2999 this.zoom.y.v2 = this.screenToY(e.offsetY);
3003 // check if cursor close to plot point
3004 if (this.param.plot[0].type === "scatter" || this.param.plot[0].type === "histogram") {
3005 let minDist = 10000;
3006 for (const [pi, p] of this.param.plot.entries()) {
3007 if (p.xData === undefined || p.xData === null)
3010 for (let i = 0; i < p.xData.length; i++) {
3011 let x = this.xToScreen(p.xData[i]);
3012 let y = this.yToScreen(p.yData[i]);
3013 let d = (e.offsetX - x) * (e.offsetX - x) +
3014 (e.offsetY - y) * (e.offsetY - y);
3017 this.marker.x = p.xData[i];
3018 this.marker.y = p.yData[i];
3021 this.marker.mx = e.offsetX;
3022 this.marker.my = e.offsetY;
3023 this.marker.plotIndex = pi;
3024 this.marker.index = i;
3029 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
3032 if (this.param.plot[0].type === "colormap") {
3033 let x = this.screenToX(e.offsetX);
3034 let y = this.screenToY(e.offsetY);
3035 let xMin = this.param.plot[0].xMin;
3036 let xMax = this.param.plot[0].xMax;
3037 let yMin = this.param.plot[0].yMin;
3038 let yMax = this.param.plot[0].yMax;
3039 let dx = (xMax - xMin) / this.nx;
3040 let dy = (yMax - yMin) / this.ny;
3041 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
3042 x > xMin && x < xMax && y > yMin && y < yMax) {
3043 let i = Math.floor((x - xMin) / dx);
3044 let j = Math.floor((y - yMin) / dy);
3046 this.marker.x = (i + 0.5) * dx + xMin;
3047 this.marker.y = (j + 0.5) * dy + yMin;
3048 this.marker.z = this.param.plot[0].zData[i + j * this.nx];
3050 this.marker.sx = this.xToScreen(this.marker.x);
3051 this.marker.sy = this.yToScreen(this.marker.y);
3052 this.marker.mx = e.offsetX;
3053 this.marker.my = e.offsetY;
3054 this.marker.plotIndex = 0;
3055 this.marker.active = true;
3057 this.marker.active = false;
3064 } else if (e.type === "wheel") {
3066 let x = this.screenToX(e.offsetX);
3067 let y = this.screenToY(e.offsetY);
3068 // Guard against scale <= -1 otherwise this.xMin becomes larger than this.xMax
3069 let scale = Math.max(e.deltaY * 0.01, -0.9);
3071 let xMinOld = this.xMin;
3072 let xMaxOld = this.xMax;
3073 let yMinOld = this.yMin;
3074 let yMaxOld = this.yMax;
3076 if (this.param.xAxis.log) {
3079 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
3081 this.xMax *= 1 + scale * (1 - f);
3082 this.xMin /= 1 + scale * f;
3084 if (this.xMax <= this.xMin) {
3085 this.xMin = xMinOld;
3086 this.xMax = xMaxOld;
3090 let dx = (this.xMax - this.xMin) * scale;
3091 let f = (x - this.xMin) / (this.xMax - this.xMin);
3092 this.xMin = this.xMin - dx * f;
3093 this.xMax = this.xMax + dx * (1 - f);
3096 // avoid too high zoom (would kill axis rendering)
3097 if (this.xMax - this.xMin < 1E-10*(this.xMax0 - this.xMin0)) {
3098 this.xMin = xMinOld;
3099 this.xMax = xMaxOld;
3102 if (this.param.yAxis.log) {
3105 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
3106 let yMinOld = this.yMin;
3107 let yMaxOld = this.yMax;
3109 this.yMax *= 1 + scale * f;
3110 this.yMin /= 1 + scale * (1 - f);
3112 if (this.yMax <= this.yMin) {
3113 this.yMin = yMinOld;
3114 this.yMax = yMaxOld;
3118 let dy = (this.yMax - this.yMin) * scale;
3119 let f = (y - this.yMin) / (this.yMax - this.yMin);
3120 this.yMin = this.yMin - dy * f;
3121 this.yMax = this.yMax + dy * (1 - f);
3124 // avoid too high zoom (would kill axis rendering)
3125 if (this.yMax - this.yMin < 1E-10*(this.yMax0 - this.yMin0)) {
3126 this.yMin = yMinOld;
3127 this.yMax = yMaxOld;
3130 this.blockAutoScale = true;
3136 this.parentDiv.title = title;
3137 this.parentDiv.style.cursor = cursor;
3142MPlotGraph.prototype.resetAxes = function () {
3144 * Reset min/max of x and y axes, redraws
3146 this.xMin = this.xMin0;
3147 this.xMax = this.xMax0;
3148 this.yMin = this.yMin0;
3149 this.yMax = this.yMax0;
3151 this.blockAutoScale = false;