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
11 * Default parameters for the MPlotGraph constructor
12 * These parameters control the formatting of the whole graph (figure)
13 * Only a subset of the below keys are needed to be passed to the MPlotGraph constructor. The rest are taken as below.
15let defaultGraphColor = [ // color sequence when adding plots
16 "#00AAFF", "#FF9000", "#FF00A0", "#00C030",
17 "#A0C0D0", "#D0A060", "#C04010", "#807060",
18 "#F0C000", "#2090A0", "#D040D0", "#90B000",
19 "#B0B040", "#B0B0FF", "#FFA0A0", "#A0FFA0"
22// Use Viridis color map from matplotlib for 2D plots
24 'color(srgb 0.267004 0.004874 0.329415)',
25 'color(srgb 0.268510 0.009605 0.335427)',
26 'color(srgb 0.269944 0.014625 0.341379)',
27 'color(srgb 0.271305 0.019942 0.347269)',
28 'color(srgb 0.272594 0.025563 0.353093)',
29 'color(srgb 0.273809 0.031497 0.358853)',
30 'color(srgb 0.274952 0.037752 0.364543)',
31 'color(srgb 0.276022 0.044167 0.370164)',
32 'color(srgb 0.277018 0.050344 0.375715)',
33 'color(srgb 0.277941 0.056324 0.381191)',
34 'color(srgb 0.278791 0.062145 0.386592)',
35 'color(srgb 0.279566 0.067836 0.391917)',
36 'color(srgb 0.280267 0.073417 0.397163)',
37 'color(srgb 0.280894 0.078907 0.402329)',
38 'color(srgb 0.281446 0.084320 0.407414)',
39 'color(srgb 0.281924 0.089666 0.412415)',
40 'color(srgb 0.282327 0.094955 0.417331)',
41 'color(srgb 0.282656 0.100196 0.422160)',
42 'color(srgb 0.282910 0.105393 0.426902)',
43 'color(srgb 0.283091 0.110553 0.431554)',
44 'color(srgb 0.283197 0.115680 0.436115)',
45 'color(srgb 0.283229 0.120777 0.440584)',
46 'color(srgb 0.283187 0.125848 0.444960)',
47 'color(srgb 0.283072 0.130895 0.449241)',
48 'color(srgb 0.282884 0.135920 0.453427)',
49 'color(srgb 0.282623 0.140926 0.457517)',
50 'color(srgb 0.282290 0.145912 0.461510)',
51 'color(srgb 0.281887 0.150881 0.465405)',
52 'color(srgb 0.281412 0.155834 0.469201)',
53 'color(srgb 0.280868 0.160771 0.472899)',
54 'color(srgb 0.280255 0.165693 0.476498)',
55 'color(srgb 0.279574 0.170599 0.479997)',
56 'color(srgb 0.278826 0.175490 0.483397)',
57 'color(srgb 0.278012 0.180367 0.486697)',
58 'color(srgb 0.277134 0.185228 0.489898)',
59 'color(srgb 0.276194 0.190074 0.493001)',
60 'color(srgb 0.275191 0.194905 0.496005)',
61 'color(srgb 0.274128 0.199721 0.498911)',
62 'color(srgb 0.273006 0.204520 0.501721)',
63 'color(srgb 0.271828 0.209303 0.504434)',
64 'color(srgb 0.270595 0.214069 0.507052)',
65 'color(srgb 0.269308 0.218818 0.509577)',
66 'color(srgb 0.267968 0.223549 0.512008)',
67 'color(srgb 0.266580 0.228262 0.514349)',
68 'color(srgb 0.265145 0.232956 0.516599)',
69 'color(srgb 0.263663 0.237631 0.518762)',
70 'color(srgb 0.262138 0.242286 0.520837)',
71 'color(srgb 0.260571 0.246922 0.522828)',
72 'color(srgb 0.258965 0.251537 0.524736)',
73 'color(srgb 0.257322 0.256130 0.526563)',
74 'color(srgb 0.255645 0.260703 0.528312)',
75 'color(srgb 0.253935 0.265254 0.529983)',
76 'color(srgb 0.252194 0.269783 0.531579)',
77 'color(srgb 0.250425 0.274290 0.533103)',
78 'color(srgb 0.248629 0.278775 0.534556)',
79 'color(srgb 0.246811 0.283237 0.535941)',
80 'color(srgb 0.244972 0.287675 0.537260)',
81 'color(srgb 0.243113 0.292092 0.538516)',
82 'color(srgb 0.241237 0.296485 0.539709)',
83 'color(srgb 0.239346 0.300855 0.540844)',
84 'color(srgb 0.237441 0.305202 0.541921)',
85 'color(srgb 0.235526 0.309527 0.542944)',
86 'color(srgb 0.233603 0.313828 0.543914)',
87 'color(srgb 0.231674 0.318106 0.544834)',
88 'color(srgb 0.229739 0.322361 0.545706)',
89 'color(srgb 0.227802 0.326594 0.546532)',
90 'color(srgb 0.225863 0.330805 0.547314)',
91 'color(srgb 0.223925 0.334994 0.548053)',
92 'color(srgb 0.221989 0.339161 0.548752)',
93 'color(srgb 0.220057 0.343307 0.549413)',
94 'color(srgb 0.218130 0.347432 0.550038)',
95 'color(srgb 0.216210 0.351535 0.550627)',
96 'color(srgb 0.214298 0.355619 0.551184)',
97 'color(srgb 0.212395 0.359683 0.551710)',
98 'color(srgb 0.210503 0.363727 0.552206)',
99 'color(srgb 0.208623 0.367752 0.552675)',
100 'color(srgb 0.206756 0.371758 0.553117)',
101 'color(srgb 0.204903 0.375746 0.553533)',
102 'color(srgb 0.203063 0.379716 0.553925)',
103 'color(srgb 0.201239 0.383670 0.554294)',
104 'color(srgb 0.199430 0.387607 0.554642)',
105 'color(srgb 0.197636 0.391528 0.554969)',
106 'color(srgb 0.195860 0.395433 0.555276)',
107 'color(srgb 0.194100 0.399323 0.555565)',
108 'color(srgb 0.192357 0.403199 0.555836)',
109 'color(srgb 0.190631 0.407061 0.556089)',
110 'color(srgb 0.188923 0.410910 0.556326)',
111 'color(srgb 0.187231 0.414746 0.556547)',
112 'color(srgb 0.185556 0.418570 0.556753)',
113 'color(srgb 0.183898 0.422383 0.556944)',
114 'color(srgb 0.182256 0.426184 0.557120)',
115 'color(srgb 0.180629 0.429975 0.557282)',
116 'color(srgb 0.179019 0.433756 0.557430)',
117 'color(srgb 0.177423 0.437527 0.557565)',
118 'color(srgb 0.175841 0.441290 0.557685)',
119 'color(srgb 0.174274 0.445044 0.557792)',
120 'color(srgb 0.172719 0.448791 0.557885)',
121 'color(srgb 0.171176 0.452530 0.557965)',
122 'color(srgb 0.169646 0.456262 0.558030)',
123 'color(srgb 0.168126 0.459988 0.558082)',
124 'color(srgb 0.166617 0.463708 0.558119)',
125 'color(srgb 0.165117 0.467423 0.558141)',
126 'color(srgb 0.163625 0.471133 0.558148)',
127 'color(srgb 0.162142 0.474838 0.558140)',
128 'color(srgb 0.160665 0.478540 0.558115)',
129 'color(srgb 0.159194 0.482237 0.558073)',
130 'color(srgb 0.157729 0.485932 0.558013)',
131 'color(srgb 0.156270 0.489624 0.557936)',
132 'color(srgb 0.154815 0.493313 0.557840)',
133 'color(srgb 0.153364 0.497000 0.557724)',
134 'color(srgb 0.151918 0.500685 0.557587)',
135 'color(srgb 0.150476 0.504369 0.557430)',
136 'color(srgb 0.149039 0.508051 0.557250)',
137 'color(srgb 0.147607 0.511733 0.557049)',
138 'color(srgb 0.146180 0.515413 0.556823)',
139 'color(srgb 0.144759 0.519093 0.556572)',
140 'color(srgb 0.143343 0.522773 0.556295)',
141 'color(srgb 0.141935 0.526453 0.555991)',
142 'color(srgb 0.140536 0.530132 0.555659)',
143 'color(srgb 0.139147 0.533812 0.555298)',
144 'color(srgb 0.137770 0.537492 0.554906)',
145 'color(srgb 0.136408 0.541173 0.554483)',
146 'color(srgb 0.135066 0.544853 0.554029)',
147 'color(srgb 0.133743 0.548535 0.553541)',
148 'color(srgb 0.132444 0.552216 0.553018)',
149 'color(srgb 0.131172 0.555899 0.552459)',
150 'color(srgb 0.129933 0.559582 0.551864)',
151 'color(srgb 0.128729 0.563265 0.551229)',
152 'color(srgb 0.127568 0.566949 0.550556)',
153 'color(srgb 0.126453 0.570633 0.549841)',
154 'color(srgb 0.125394 0.574318 0.549086)',
155 'color(srgb 0.124395 0.578002 0.548287)',
156 'color(srgb 0.123463 0.581687 0.547445)',
157 'color(srgb 0.122606 0.585371 0.546557)',
158 'color(srgb 0.121831 0.589055 0.545623)',
159 'color(srgb 0.121148 0.592739 0.544641)',
160 'color(srgb 0.120565 0.596422 0.543611)',
161 'color(srgb 0.120092 0.600104 0.542530)',
162 'color(srgb 0.119738 0.603785 0.541400)',
163 'color(srgb 0.119512 0.607464 0.540218)',
164 'color(srgb 0.119423 0.611141 0.538982)',
165 'color(srgb 0.119483 0.614817 0.537692)',
166 'color(srgb 0.119699 0.618490 0.536347)',
167 'color(srgb 0.120081 0.622161 0.534946)',
168 'color(srgb 0.120638 0.625828 0.533488)',
169 'color(srgb 0.121380 0.629492 0.531973)',
170 'color(srgb 0.122312 0.633153 0.530398)',
171 'color(srgb 0.123444 0.636809 0.528763)',
172 'color(srgb 0.124780 0.640461 0.527068)',
173 'color(srgb 0.126326 0.644107 0.525311)',
174 'color(srgb 0.128087 0.647749 0.523491)',
175 'color(srgb 0.130067 0.651384 0.521608)',
176 'color(srgb 0.132268 0.655014 0.519661)',
177 'color(srgb 0.134692 0.658636 0.517649)',
178 'color(srgb 0.137339 0.662252 0.515571)',
179 'color(srgb 0.140210 0.665859 0.513427)',
180 'color(srgb 0.143303 0.669459 0.511215)',
181 'color(srgb 0.146616 0.673050 0.508936)',
182 'color(srgb 0.150148 0.676631 0.506589)',
183 'color(srgb 0.153894 0.680203 0.504172)',
184 'color(srgb 0.157851 0.683765 0.501686)',
185 'color(srgb 0.162016 0.687316 0.499129)',
186 'color(srgb 0.166383 0.690856 0.496502)',
187 'color(srgb 0.170948 0.694384 0.493803)',
188 'color(srgb 0.175707 0.697900 0.491033)',
189 'color(srgb 0.180653 0.701402 0.488189)',
190 'color(srgb 0.185783 0.704891 0.485273)',
191 'color(srgb 0.191090 0.708366 0.482284)',
192 'color(srgb 0.196571 0.711827 0.479221)',
193 'color(srgb 0.202219 0.715272 0.476084)',
194 'color(srgb 0.208030 0.718701 0.472873)',
195 'color(srgb 0.214000 0.722114 0.469588)',
196 'color(srgb 0.220124 0.725509 0.466226)',
197 'color(srgb 0.226397 0.728888 0.462789)',
198 'color(srgb 0.232815 0.732247 0.459277)',
199 'color(srgb 0.239374 0.735588 0.455688)',
200 'color(srgb 0.246070 0.738910 0.452024)',
201 'color(srgb 0.252899 0.742211 0.448284)',
202 'color(srgb 0.259857 0.745492 0.444467)',
203 'color(srgb 0.266941 0.748751 0.440573)',
204 'color(srgb 0.274149 0.751988 0.436601)',
205 'color(srgb 0.281477 0.755203 0.432552)',
206 'color(srgb 0.288921 0.758394 0.428426)',
207 'color(srgb 0.296479 0.761561 0.424223)',
208 'color(srgb 0.304148 0.764704 0.419943)',
209 'color(srgb 0.311925 0.767822 0.415586)',
210 'color(srgb 0.319809 0.770914 0.411152)',
211 'color(srgb 0.327796 0.773980 0.406640)',
212 'color(srgb 0.335885 0.777018 0.402049)',
213 'color(srgb 0.344074 0.780029 0.397381)',
214 'color(srgb 0.352360 0.783011 0.392636)',
215 'color(srgb 0.360741 0.785964 0.387814)',
216 'color(srgb 0.369214 0.788888 0.382914)',
217 'color(srgb 0.377779 0.791781 0.377939)',
218 'color(srgb 0.386433 0.794644 0.372886)',
219 'color(srgb 0.395174 0.797475 0.367757)',
220 'color(srgb 0.404001 0.800275 0.362552)',
221 'color(srgb 0.412913 0.803041 0.357269)',
222 'color(srgb 0.421908 0.805774 0.351910)',
223 'color(srgb 0.430983 0.808473 0.346476)',
224 'color(srgb 0.440137 0.811138 0.340967)',
225 'color(srgb 0.449368 0.813768 0.335384)',
226 'color(srgb 0.458674 0.816363 0.329727)',
227 'color(srgb 0.468053 0.818921 0.323998)',
228 'color(srgb 0.477504 0.821444 0.318195)',
229 'color(srgb 0.487026 0.823929 0.312321)',
230 'color(srgb 0.496615 0.826376 0.306377)',
231 'color(srgb 0.506271 0.828786 0.300362)',
232 'color(srgb 0.515992 0.831158 0.294279)',
233 'color(srgb 0.525776 0.833491 0.288127)',
234 'color(srgb 0.535621 0.835785 0.281908)',
235 'color(srgb 0.545524 0.838039 0.275626)',
236 'color(srgb 0.555484 0.840254 0.269281)',
237 'color(srgb 0.565498 0.842430 0.262877)',
238 'color(srgb 0.575563 0.844566 0.256415)',
239 'color(srgb 0.585678 0.846661 0.249897)',
240 'color(srgb 0.595839 0.848717 0.243329)',
241 'color(srgb 0.606045 0.850733 0.236712)',
242 'color(srgb 0.616293 0.852709 0.230052)',
243 'color(srgb 0.626579 0.854645 0.223353)',
244 'color(srgb 0.636902 0.856542 0.216620)',
245 'color(srgb 0.647257 0.858400 0.209861)',
246 'color(srgb 0.657642 0.860219 0.203082)',
247 'color(srgb 0.668054 0.861999 0.196293)',
248 'color(srgb 0.678489 0.863742 0.189503)',
249 'color(srgb 0.688944 0.865448 0.182725)',
250 'color(srgb 0.699415 0.867117 0.175971)',
251 'color(srgb 0.709898 0.868751 0.169257)',
252 'color(srgb 0.720391 0.870350 0.162603)',
253 'color(srgb 0.730889 0.871916 0.156029)',
254 'color(srgb 0.741388 0.873449 0.149561)',
255 'color(srgb 0.751884 0.874951 0.143228)',
256 'color(srgb 0.762373 0.876424 0.137064)',
257 'color(srgb 0.772852 0.877868 0.131109)',
258 'color(srgb 0.783315 0.879285 0.125405)',
259 'color(srgb 0.793760 0.880678 0.120005)',
260 'color(srgb 0.804182 0.882046 0.114965)',
261 'color(srgb 0.814576 0.883393 0.110347)',
262 'color(srgb 0.824940 0.884720 0.106217)',
263 'color(srgb 0.835270 0.886029 0.102646)',
264 'color(srgb 0.845561 0.887322 0.099702)',
265 'color(srgb 0.855810 0.888601 0.097452)',
266 'color(srgb 0.866013 0.889868 0.095953)',
267 'color(srgb 0.876168 0.891125 0.095250)',
268 'color(srgb 0.886271 0.892374 0.095374)',
269 'color(srgb 0.896320 0.893616 0.096335)',
270 'color(srgb 0.906311 0.894855 0.098125)',
271 'color(srgb 0.916242 0.896091 0.100717)',
272 'color(srgb 0.926106 0.897330 0.104071)',
273 'color(srgb 0.935904 0.898570 0.108131)',
274 'color(srgb 0.945636 0.899815 0.112838)',
275 'color(srgb 0.955300 0.901065 0.118128)',
276 'color(srgb 0.964894 0.902323 0.123941)',
277 'color(srgb 0.974417 0.903590 0.130215)',
278 'color(srgb 0.983868 0.904867 0.136897)',
279 'color(srgb 0.993248 0.906157 0.143936)'
282let defaultGraphParam = {
284 type: undefined, // one of "scatter", "histogram", "colormap", "bar"
285 showMenuButtons: true, // if false hide menu buttons
286 mouseWheelZoom: true, // if false disable mouse scroll zoom
288 floating: false, // true if in a floating dialog box
292 background: "#FFFFFF", // background, default is white
293 axis: "#808080", // axes lines and ticks color
294 grid: "#D0D0D0", // grid lines color
295 label: "#404040", // colors of the axis labels
300 backgroundColor: "#808080",
301 textSize: 16, // font size
302 text: "" // text content for the title
308 backgroundColor: "#FFFFFF",
309 textColor: "#404040",
313 barWidth: 0.3, // width of the bars in a bar plot [0, 1], where 1 fills the entire space between points
315 stats: { // the following are set by calcStats
317 names: [], // names of the various stats, varies by plot type
322 type: "numeric", // One of "numeric", "datetime", "category", "datetime"
323 log: false, // logscale if true
324 min: undefined, // axis limits
325 max: undefined, // axis limits
326 grid: true, // if true extend tick marks into a grid
327 textSize: 16, // size of the axis tick mark lables
329 text: "", // axis label text
330 textSize: 16 // font size of the axis label text
334 yAxis: { // This axis is always "numeric"
335 log: false, // logscale if true
336 min: undefined, // axis limits
337 max: undefined, // axis limits
338 grid: true, // if true extend tick marks into a grid
339 textSize: 16, // size of the axis tick mark lables
341 text: "", // axis label text
342 textSize: 16 // font size of the axis label text
346 zAxis: { // This axis is always "numeric"
347 show: true, // logscale if true
348 min: undefined, // axis limits
349 max: undefined, // axis limits
350 textSize: 16, // size of the axis tick mark lables
352 text: "", // axis label text
353 textSize: 16 // font size of the axis label text
357 plot: [] // this array holds the parameters for each plot (dataset). See defaultPlotParam
361 * Default parameters for the MPlotGraph.addPlot() function
362 * These parameters control the formatting for each plot (read: dataset) added into the graph
363 * Only a subset of the below keys are needed to be passed to the addPlot function. The rest are taken as below.
365let defaultPlotParam = {
366 type: "scatter", // One of "scatter", "histogram", "colormap", "bar"
367 odbPath: "", // path in the ODB to the directory with the data. Prefix to the below paths.
369 xPath: "", // ODB path to x data
370 yPath: "", // ODB path to y data
371 zPath: "", // ODB path to z data
372 xErrorPath: "", // ODB path to x error data
373 yErrorPath: "", // ODB path to y error data
375 label: "", // label for this plot
376 alpha: 1, // transparency [0,1]. Zero is completely transparent.
377 zeroColor: undefined, // color used for colormaps with contents < 0.5
379 nx: 0, // number of x points
380 ny: 0, // number of y points
381 xData: [], // data array x values
382 yData: [], // data array y values
383 zData: [], // data array z values
384 xErrorData: [], // data array x errors
385 yErrorData: [], // data array y errors
387 xMin: undefined, // drawing limits
388 xMax: undefined, // drawing limits
389 yMin: undefined, // drawing limits
390 yMax: undefined, // drawing limits
392 marker: { // marker properties at each point
393 draw: true, // if false, don't draw the markers
394 lineColor: "", // color of the marker border. See defaultGraphParam.color.data for colors
395 fillColor: "", // color of the fill. See defaultGraphParam.color.data for colors
396 style: "circle", // One of "none", "circle", "square", "diamond", "pentagon", "triangle-up", "triangle-down", "triangle-left", "triangle-right", "cross", "plus"
398 lineWidth: 2 // marker border width
401 line: { // properties for lines interconnecting points
402 draw: true, // if false don't draw the line
403 fill: false, // fill the space below th eline
404 color: "", // color of the line. See defaultGraphParam.color.data for colors
405 style: "solid",// One of "none", "solid", "dashed", "dotted"
406 width: 2 // line width
410/**Initialize <div> elements of class "mplot".
411 * Loads the data into the graphs
412 * Can only be called after all data has been created
414function mplot_init() {
416 // go through all data-name="mplot" tags
417 let mPlot = document.getElementsByClassName("mplot");
419 for (let i = 0; i < mPlot.length; i++)
420 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
424 window.addEventListener('resize', windowResize);
427function profile(flag) {
428 if (flag === true || flag === undefined) {
430 profile.startTime = new Date().getTime();
434 let now = new Date().getTime();
435 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
436 profile.startTime = new Date().getTime();
439/** Resize all mplot objects as defined by their class */
440function windowResize() {
441 let mPlot = document.getElementsByClassName("mplot");
442 for (const m of mPlot)
446function isObject(item) {
447 return (item && typeof item === 'object' && !Array.isArray(item));
450String.prototype.stripZeros = function () {
452 if (s.search("[.]") >= 0) {
453 let i = s.search("[e]");
455 while (s.charAt(i - 1) === "0") {
456 s = s.substring(0, i - 1) + s.substring(i);
459 if (s.charAt(i - 1) === ".")
460 s = s.substring(0, i - 1) + s.substring(i);
462 while (s.charAt(s.length - 1) === "0")
463 s = s.substring(0, s.length - 1);
464 if (s.charAt(s.length - 1) === ".")
465 s = s.substring(0, s.length - 1);
471CanvasRenderingContext2D.prototype.drawLine = function (x1, y1, x2, y2) {
478/** Recursively merge object keys from source into target. Modifies in-place.
479 * @param {object} target the object to make new keys or overwrite existing ones
480 * @param {object} source the object from which the keys are copied
481 * @returns {object} the target with the new keys
483function deepMerge(target, source) {
484 for (let key in source) {
485 if (source.hasOwnProperty(key)) {
486 if (isObject(source[key])) {
487 if (!target[key]) Object.assign(target, { [key]: {} });
488 deepMerge(target[key], source[key]);
490 Object.assign(target, { [key]: source[key] });
497/** Load data from the ODB for all HTML elements with the mplot class name */
498function loadMPlotData() {
500 // go through all data-name="mplot" tags
501 let mPlot = document.getElementsByClassName("mplot");
504 for (const mp of mPlot) {
505 for (const pl of mp.mpg.param.plot) {
506 if (pl.odbPath === undefined || pl.odbPath === "")
513 if ((pl.type === "scatter" || pl.type === "histogram") &&
514 (pl.yPath === undefined || pl.yPath === null || pl.yPath === "")) {
515 mp.mpg.error ="Invalid Y data \"" + pl.yPath + "\" for " + pl.type + " plot \"" + name+ "\"";
521 if ((pl.type === "colormap") &&
522 (pl.zPath === undefined || pl.zPath === null || pl.zPath === "")) {
523 mp.mpg.error = "Invalid Z data \"" + pl.zPath + "\" for colormap plot \"" + name + "\"";
529 if (pl.odbPath.slice(-1) !== '/')
532 if (pl.xPath !== undefined && pl.xPath !== null && pl.xPath !== "")
533 v.push(pl.odbPath + pl.xPath);
534 if (pl.yPath !== undefined && pl.yPath !== null && pl.yPath !== "")
535 v.push(pl.odbPath + pl.yPath);
536 if (pl.zPath !== undefined && pl.zPath !== null && pl.zPath !== "")
537 v.push(pl.odbPath + pl.zPath);
538 if (pl.xErrorPath !== undefined && pl.xErrorPath !== null && pl.xErrorPath !== "")
539 v.push(pl.odbPath + pl.xErrorPath);
540 if (pl.yErrorPath !== undefined && pl.yErrorPath !== null && pl.yErrorPath !== "")
541 v.push(pl.odbPath + pl.yErrorPath);
545 mjsonrpc_db_get_values(v).then( function(rpc) {
547 let mPlot = document.getElementsByClassName("mplot");
550 for (let mp of mPlot) {
551 for (let plt of mp.mpg.param.plot) {
552 if (!plt.odbPath === undefined || plt.odbPath === "" || plt.invalid)
555 let name = plt.label;
559 if (plt.xPath !== undefined && plt.xPath !== null && plt.xPath !== "") {
560 plt.xData = rpc.result.data[i++];
561 if (plt.xData === null)
562 mp.mpg.error = "Invalid X data \"" + plt.xPath + "\" for plot \"" + name + "\"";
563 if (Array.isArray(plt.xData) && plt.xData.length > 0 &&
564 plt.xData.every(item => typeof item === "string")) {
566 // switch plot to category plot if sting array found for x-data
568 plt.marker = undefined;
569 mp.mpg.param.xAxis.type = "category";
572 if (plt.yPath !== undefined && plt.yPath !== null && plt.yPath !== "") {
573 plt.yData = rpc.result.data[i++];
574 if (plt.yData === null)
575 mp.mpg.error = "Invalid Y data \"" + plt.yPath + "\" for plot \"" + name + "\"";
577 if (plt.zPath !== undefined && plt.zPath !== null && plt.zPath !== "") {
578 plt.zData = rpc.result.data[i++];
579 if (plt.zData === null)
580 mp.mpg.error = "Invalid Z data \"" + plt.zPath + "\" for plot \"" + name + "\"";
582 if (plt.xErrorPath !== undefined && plt.xErrorPath !== null && plt.xErrorPath !== "") {
583 plt.xErrorData = rpc.result.data[i++];
584 if (plt.xErrorData === null)
585 mp.mpg.error = "Invalid X error data \"" + plt.xErrorPath + "\" for plot \"" + name + "\"";
587 if (plt.yErrorPath !== undefined && plt.yErrorPath !== null && plt.yErrorPath !== "") {
588 plt.yErrorData = rpc.result.data[i++];
589 if (plt.yErrorData === null)
590 mp.mpg.error = "Invalid Y error data \"" + plt.yError + "\" for plot \"" + name + "\"";
593 if ((plt.type === "scatter" || plt.type === "histogram" || plt.type === "bar") &&
594 mp.mpg.error === null) {
595 // generate X data for histograms and category plots
596 if (plt.xData === undefined || plt.xData.length === 0 || plt.type === "bar") {
597 if (plt.type === "scatter") {
598 // scatter plot goes from 0 ... N
600 plt.xMax = plt.yData.length;
601 plt.xData = Array.from({length: plt.yData.length}, (v, i) => i);
602 } else if (plt.type === "histogram") {
603 // histogram goes from -0.5 ... N-0.5 to have bins centered over bin x-value
605 plt.xMax = plt.yData.length - 0.5;
607 let dx = (plt.xMax - plt.xMin) / plt.yData.length;
608 let x0 = plt.xMin + dx / 2;
609 plt.xData = Array.from({length: plt.yData.length}, (v, i) => x0 + i * dx);
610 } else if (plt.type === "bar") {
612 plt.xMax = plt.xData.length;
615 plt.xMin = Math.min(...plt.xData);
616 plt.xMax = Math.max(...plt.xData);
619 plt.yMin = Math.min(...plt.yData);
620 plt.yMax = Math.max(...plt.yData);
622 if (plt.type === "bar")
626 if (plt.type === "colormap" && mp.mpg.error === null) {
627 plt.zMin = Math.min(...plt.zData.filter(v=>!isNaN(v)));
628 plt.zMax = Math.max(...plt.zData.filter(v=>!isNaN(v)));
630 if (plt.xMin === undefined) {
632 plt.xMax = plt.nx - 0.5;
634 if (plt.yMin === undefined) {
636 plt.yMax = plt.ny - 0.5;
639 let dx = (plt.xMax - plt.xMin) / plt.nx;
640 let x0 = plt.xMin + dx/2;
641 plt.xData = Array.from({length: plt.nx}, (v,i) => x0 + i*dx);
643 let dy = (plt.yMax - plt.yMin) / plt.ny;
644 let y0 = plt.yMin + dy/2;
645 plt.yData = Array.from({length: plt.ny}, (v,i) => y0 + i*dy);
652 for (const mp of mPlot) {
653 if (!mp.mpg.blockAutoScale)
658 // refresh data once per second
659 window.setTimeout(loadMPlotData, 1000);
661 }).catch( (error) => {
670/* Begin ptimeToLabel format options */
673 day: '2-digit', month: 'short', year: '2-digit',
674 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
679 day: '2-digit', month: 'short', year: '2-digit',
680 hour12: false, hour: '2-digit', minute: '2-digit'
685 day: '2-digit', month: 'short', year: '2-digit',
686 hour12: false, hour: '2-digit', minute: '2-digit'
691 day: '2-digit', month: 'short', year: '2-digit'
696 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
701 hour12: false, hour: '2-digit', minute: '2-digit'
706 hour12: false, hour: '2-digit', minute: '2-digit'
711 day: '2-digit', month: 'short', year: '2-digit',
712 hour12: false, hour: '2-digit', minute: '2-digit'
717 day: '2-digit', month: 'short', year: '2-digit'
719/* End ptimeToLabel format options */
721/** Convert time in seconds to a human-readable string
722 * @param {number} sec number of seconds
723 * @param {number} base chooses which display option to use on conversion
724 * @param {bool} forceDate if true force showing the date, else can show only time
725 * @returns {string} human-readable datetime as a string
727function ptimeToLabel(sec, base, forceDate) {
728 let d = mhttpd_get_display_time(sec).date;
732 return d.toLocaleTimeString('en-GB', poptions1);
733 } else if (base < 600) {
734 return d.toLocaleTimeString('en-GB', poptions2);
735 } else if (base < 3600 * 24) {
736 return d.toLocaleTimeString('en-GB', poptions3);
738 return d.toLocaleDateString('en-GB', poptions4);
743 return d.toLocaleTimeString('en-GB', poptions5);
744 } else if (base < 600) {
745 return d.toLocaleTimeString('en-GB', poptions6);
746 } else if (base < 3600 * 3) {
747 return d.toLocaleTimeString('en-GB', poptions7);
748 } else if (base < 3600 * 24) {
749 return d.toLocaleTimeString('en-GB', poptions8);
751 return d.toLocaleDateString('en-GB', poptions9);
755function checkPlotType(type) {
756 return type === "histogram" || type === "bar" || type === "scatter" ||
760function mpgDialog(p) {
761 let width = window.innerWidth-200;
762 let height = window.innerHeight-200;
764 let dlgFrame = document.createElement("div");
765 dlgFrame.className = "dlgFrame";
766 dlgFrame.style.zIndex = "30";
767 dlgFrame.style.backgroundColor = "white";
768 dlgFrame.style.overflow = "hidden";
769 dlgFrame.style.width = width + "px";
770 dlgFrame.style.height = height + "px";
771 dlgFrame.style.minWidth = "100px"; // overwrite max-content
772 dlgFrame.style.minHeight = "50px"; // overwrite max-content
773 dlgFrame.shouldDestroy = true;
775 let dlgTitle = document.createElement("div");
776 dlgTitle.className = "dlgTitlebar";
777 dlgTitle.id = "dlgMessageTitle";
778 dlgTitle.innerText = p.param?.title?.text;
779 dlgFrame.appendChild(dlgTitle);
780 document.body.appendChild(dlgFrame);
783 let dlgPanel = document.createElement("div");
784 dlgPanel.className = "dlgPanel";
785 dlgPanel.style.flex = "1 1 auto";
786 dlgPanel.style.width = (width - 2) + "px";
787 dlgPanel.style.height = (height - dlgTitle.clientHeight - 2) + "px";
788 dlgPanel.style.minWidth = ""; // overwrite max-content
789 dlgPanel.style.minHeight = ""; // overwrite max-content
790 dlgPanel.style.padding = "0";
791 dlgPanel.style.border = "";
792 dlgPanel.style.backgroundColor = "transparent";
793 dlgFrame.appendChild(dlgPanel);
795 p.param.floating = true;
796 dlgPanel.mpg = new MPlotGraph(dlgPanel, p.param);
797 dlgPanel.mpg.param.title.text = ""; // title is shown in the dialog title instead
798 dlgPanel.mpg.param.floating = true;
799 dlgPanel.mpg.canvas.width = parseInt(dlgPanel.style.width);
800 dlgPanel.mpg.canvas.height = parseInt(dlgPanel.style.height);
801 dlgPanel.mpg.canvas.style.border = "none";
802 dlgPanel.mpg.calcMinMax();
805 new ResizeObserver(() => {
806 const rect = dlgFrame.getBoundingClientRect();
807 dlgPanel.style.width = (rect.width - 4) + "px";
808 dlgPanel.style.height = (rect.height - dlgTitle.clientHeight - 4) + "px";
810 window.setTimeout(() => {
811 dlgPanel.mpg.canvas.width = parseInt(dlgPanel.style.width);
812 dlgPanel.mpg.canvas.height = parseInt(dlgPanel.style.height);
813 dlgPanel.mpg.redraw();
815 }).observe(dlgFrame);
818/** Umbrella figure object containing the axes, buttons, and all plots.
819 * MPlotGraph redraws all contained elements upon any change to the figure (zoom, pan, new line, etc).
820 * For most drawing applications you will need only draw, and addPlot or setData.
826 * @param {Object} divElement HTML div element to place the plot into
827 * @param {Object} graphParam JS object with keys a subset of {@link defaultGraphParam}, controls plot display style.
829 * let graph = MPlotGraph();
830 * graph.addPlot({xData:[1,2,3], yData:[4,5,6]});
833 constructor(divElement, graphParam) {
835 if (divElement === undefined || divElement === null) {
836 dlgAlert("MPlot constructor called without valid divElement");
840 // save parameters from <div>
841 this.parentDiv = divElement;
842 this.divParam = divElement.innerHTML;
843 divElement.innerHTML = "";
845 // if absent, generate random string (5 char) to give an id to parent element
846 if (!this.parentDiv.id)
847 this.parentDiv.id = (Math.random() + 1).toString(36).substring(7);
849 // default parameters
850 this.param = JSON.parse(JSON.stringify(defaultGraphParam)); // deep copy
852 // overwrite default parameters from <div> text body
854 if (this.divParam.includes('{')) {
855 let p = JSON.parse(this.divParam);
856 this.param = deepMerge(this.param, p);
859 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
864 // obtain parameters form <div> attributes ---
867 if (this.parentDiv.dataset.odbPath)
868 this.param.odbPath = this.parentDiv.dataset.odbPath;
871 if (this.parentDiv.dataset.type)
872 this.param.type = this.parentDiv.dataset.type;
875 if (this.parentDiv.dataset.title)
876 this.param.title.text = this.parentDiv.dataset.title;
879 if (this.parentDiv.dataset.xText)
880 this.param.xAxis.title.text = this.parentDiv.dataset.xText;
881 if (this.parentDiv.dataset.yText)
882 this.param.yAxis.title.text = this.parentDiv.dataset.yText;
883 if (this.parentDiv.dataset.zText)
884 this.param.zAxis.title.text = this.parentDiv.dataset.zText;
887 if (this.parentDiv.dataset.x || this.parentDiv.dataset.xError) {
888 if (this.param.plot.length === 0)
889 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
890 this.param.plot[0].odbPath = this.param.odbPath;
891 this.param.plot[0].type = "scatter";
894 if (this.parentDiv.dataset.x)
895 this.param.plot[0].xPath = this.parentDiv.dataset.x;
896 if (this.parentDiv.dataset.y)
897 this.param.plot[0].yPath = this.parentDiv.dataset.y;
900 if (this.parentDiv.dataset.xError)
901 this.param.plot[0].xError = this.parentDiv.dataset.xError;
902 if (this.parentDiv.dataset.yError)
903 this.param.plot[0].yError = this.parentDiv.dataset.yError;
905 // data-x/y/z-min/max
906 if (this.parentDiv.dataset.xMin)
907 this.param.xAxis.min = parseFloat(this.parentDiv.dataset.xMin);
908 if (this.parentDiv.dataset.xMax)
909 this.param.xAxis.max = parseFloat(this.parentDiv.dataset.xMax);
910 if (this.parentDiv.dataset.yMin)
911 this.param.yAxis.min = parseFloat(this.parentDiv.dataset.yMin);
912 if (this.parentDiv.dataset.yMax)
913 this.param.yAxis.max = parseFloat(this.parentDiv.dataset.yMax);
914 if (this.parentDiv.dataset.zMin)
915 this.param.zAxis.min = parseFloat(this.parentDiv.dataset.zMin);
916 if (this.parentDiv.dataset.zMax)
917 this.param.zAxis.max = parseFloat(this.parentDiv.dataset.zMax);
920 if (this.parentDiv.dataset.xLog)
921 this.param.xAxis.log = this.parentDiv.dataset.xLog === "true" || this.parentDiv.dataset.xLog === "1";
922 if (this.parentDiv.dataset.yLog)
923 this.param.yAxis.log = this.parentDiv.dataset.yLog === "true" || this.parentDiv.dataset.yLog === "1";
924 if (this.parentDiv.dataset.zLog) {
925 this.param.zAxis.log = this.parentDiv.dataset.zLog === "true" || this.parentDiv.dataset.zLog === "1";
926 if (this.param.zAxis.log) {
927 if (this.param.zAxis.min < 1E-20)
928 this.param.zAxis.min = 1E-20;
929 if (this.param.zAxis.max < 1E-18)
930 this.param.zAxis.max = 1E-18;
935 if (this.parentDiv.dataset.h) {
936 if (this.param.plot.length === 0)
937 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
938 this.param.plot[0].odbPath = this.param.odbPath;
939 this.param.plot[0].type = "histogram";
940 this.param.plot[0].yPath = this.parentDiv.dataset.h;
941 this.param.plot[0].line.color = "#404040";
942 this.param.plot[0].marker.draw = false;
943 if (!this.parentDiv.dataset.x) {
944 this.param.plot[0].xMin = this.param.xAxis.min;
945 this.param.plot[0].xMax = this.param.xAxis.max;
950 if (this.parentDiv.dataset.z) {
951 if (this.param.plot.length === 0)
952 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
953 this.param.plot[0].odbPath = this.param.odbPath;
954 this.param.plot[0].type = "colormap";
955 this.param.plot[0].showZScale = true;
956 this.param.plot[0].zeroColor = this.parentDiv.dataset.zeroColor;
957 this.param.plot[0].zPath = this.parentDiv.dataset.z;
958 this.param.plot[0].xMin = this.param.xAxis.min;
959 this.param.plot[0].xMax = this.param.xAxis.max;
960 this.param.plot[0].yMin = this.param.yAxis.min;
961 this.param.plot[0].yMax = this.param.yAxis.max;
962 this.param.plot[0].zMin = this.param.zAxis.min;
963 this.param.plot[0].zMax = this.param.zAxis.max;
964 this.param.plot[0].nx = parseInt(this.parentDiv.dataset.nx);
965 this.param.plot[0].ny = parseInt(this.parentDiv.dataset.ny);
966 if (this.param.plot[0].nx === undefined) {
967 dlgAlert("\"data-nx\" missing for colormap mplot <div>");
970 if (this.param.plot[0].ny === undefined) {
971 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
977 if (this.parentDiv.dataset.label)
978 this.param.plot[0].label = this.parentDiv.dataset.label;
981 if (this.parentDiv.dataset["line-width"])
982 this.param.plot[0].line.width = this.parentDiv.dataset["line-width"];
985 if (this.parentDiv.dataset["bar-width"])
986 this.param.barWidth = this.parentDiv.dataset["bar-width"];
989 if (this.parentDiv.dataset["marker-style"])
990 this.param.plot[0].marker.style = this.parentDiv.dataset["marker-style"];
992 // data-x<n>/y<n>/label<n>/alpha<n>/marker<n>
993 for (let i=1, index=0 ; i<16 ; i++, index++) {
994 if (this.parentDiv.dataset["y"+i]) { // use y here since bar plot could only have "data-x"
995 if (i > this.param.plot.length)
996 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
997 this.param.plot[index].odbPath = this.param.odbPath;
998 this.param.plot[index].marker.lineColor = defaultGraphColor[index];
999 this.param.plot[index].marker.fillColor = defaultGraphColor[index];
1000 this.param.plot[index].line.color = defaultGraphColor[index];
1002 if (this.parentDiv.dataset["x" + i])
1003 this.param.plot[index].xPath = this.parentDiv.dataset["x" + i];
1005 // copy from first plot if we only have "data-x"
1006 this.param.plot[index].xPath = this.param.plot[0].xPath;
1008 if (this.parentDiv.dataset["y" + i])
1009 this.param.plot[index].yPath = this.parentDiv.dataset["y" + i];
1010 if (this.parentDiv.dataset["x" + i + "Error"])
1011 this.param.plot[index].xErrorPath = this.parentDiv.dataset["x" + i + "Error"];
1012 if (this.parentDiv.dataset["y" + i + "Error"])
1013 this.param.plot[index].yErrorPath = this.parentDiv.dataset["y" + i + "Error"];
1014 if (this.parentDiv.dataset["label" + i])
1015 this.param.plot[index].label = this.parentDiv.dataset["label" + i];
1016 if (this.parentDiv.dataset["alpha" + i])
1017 this.param.plot[index].alpha = parseFloat(this.parentDiv.dataset["alpha" + i]);
1018 if (this.parentDiv.dataset["line" + i + "-width"])
1019 this.param.plot[index].line.width = parseFloat(this.parentDiv.dataset["line" + i + "-width"]);
1020 if (this.parentDiv.dataset["line" + i + "-style"])
1021 this.param.plot[index].line.style = this.parentDiv.dataset["line" + i + "-style"];
1022 if (this.parentDiv.dataset["marker" + i + "-style"])
1023 this.param.plot[index].marker.style = this.parentDiv.dataset["marker" + i + "-style"];
1028 for (let i=1, index=0 ; i<16 ; i++, index++) {
1029 if (this.parentDiv.dataset["h"+i]) {
1030 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
1031 this.param.plot[index].odbPath = this.param.odbPath;
1032 this.param.plot[index].marker.lineColor = defaultGraphColor[index];
1033 this.param.plot[index].marker.fillColor = defaultGraphColor[index];
1034 this.param.plot[index].line.color = defaultGraphColor[index];
1036 this.param.plot[index].type = "histogram";
1037 this.param.plot[index].yPath = this.parentDiv.dataset["h"+i];
1039 this.param.plot[index].xMin = this.param.xAxis.min;
1040 this.param.plot[index].xMax = this.param.xAxis.max;
1042 if (this.parentDiv.dataset["label"+i])
1043 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
1048 if (this.parentDiv.dataset["x-type"])
1049 this.param.xAxis.type = this.parentDiv.dataset["x-type"];
1052 if (this.parentDiv.dataset.overlay) {
1053 this.param.overlay = this.parentDiv.dataset.overlay;
1054 if (this.param.overlay.indexOf('(') !== -1) // strip any '('
1055 this.param.overlay = this.param.overlay.substring(0, this.param.overlay.indexOf('('));
1059 if (this.parentDiv.dataset.event) {
1060 this.param.event = this.parentDiv.dataset.event;
1061 if (this.param.event.indexOf('(') !== -1) // strip any '('
1062 this.param.event = this.param.event.substring(0, this.param.event.indexOf('('));
1066 if (this.parentDiv.dataset.stats)
1067 this.param.stats.show = (this.parentDiv.dataset.stats === "1");
1069 // set parameters from constructor
1071 deepMerge(this.param, graphParam);
1074 for (const p of this.param.plot) {
1075 if (!checkPlotType(p.type))
1076 throw new Error(`mplot.js: Unknown plot type "${p.type}"`);
1099 this.marker = {active: false};
1100 this.blockAutoScale = false;
1107 src: "maximize.svg",
1108 title: "Maximize this plot",
1109 click: function (t) {
1115 title: "Show / hide legend",
1116 click: function (t) {
1117 t.param.legend.show = !t.param.legend.show;
1120 for (const g of t.param.plot)
1121 if (g.label && g.label !== "")
1124 dlgAlert("No plot labels defined");
1131 title: "Show / hide statistics",
1132 click: function (t) {
1133 t.param.stats.show = !t.param.stats.show;
1138 src: "rotate-ccw.svg",
1139 title: "Reset histogram axes",
1140 click: function (t) {
1145 src: "download.svg",
1146 title: "Download image/data...",
1147 click: function (t) {
1148 if (t.downloadSelector.style.display === "none") {
1149 t.downloadSelector.style.display = "block";
1150 let w = t.downloadSelector.getBoundingClientRect().width;
1151 t.downloadSelector.style.left = (t.canvas.getBoundingClientRect().x + window.scrollX +
1152 t.width - 26 - w) + "px";
1153 t.downloadSelector.style.top = (t.canvas.getBoundingClientRect().y + window.scrollY +
1155 t.downloadSelector.style.zIndex = "32";
1157 t.downloadSelector.style.display = "none";
1163 // remove maximize button if we are floating
1164 if (this.param.floating)
1165 this.button.shift();
1167 this.button.forEach(b => {
1168 b.img = new Image();
1169 b.img.src = "icons/" + b.src;
1172 this.createDownloadSelector();
1174 // mouse event handlers
1175 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
1176 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
1177 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
1178 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
1179 divElement.addEventListener("wheel", this.mouseEvent.bind(this), true);
1181 // Keyboard event handler (has to be on the window!)
1182 window.addEventListener("keydown", this.keyDown.bind(this));
1185 this.canvas = document.createElement("canvas");
1186 this.canvas.style.border = "1px solid black";
1188 if (parseInt(this.parentDiv.style.width) > 0)
1189 this.canvas.width = parseInt(this.parentDiv.style.width);
1191 this.canvas.width = 500;
1192 if (parseInt(this.parentDiv.style.height) > 0)
1193 this.canvas.height = parseInt(this.parentDiv.style.height);
1195 this.canvas.height = 300;
1197 divElement.appendChild(this.canvas);
1200 /** Add a plot to the graph
1201 * Plot drawing info is stored in the MPlotGraph.param.plot array. Parameters
1202 * can be modified in realtime by editing this array directly
1203 * @param {Object} plotParam subset of keys from the {@link defaultPlotParam} object
1204 * @returns {int} index of the plot added in the MPlotGraph.param.plot array
1205 * @example let index = plt.addPlot({xData:[1,2,3], yData:[4,5,6], yErrorData=[1,1,2]});
1207 addPlot(plotParam) {
1208 this.param.plot.push(JSON.parse(JSON.stringify(defaultPlotParam)));
1209 let index = this.param.plot.length - 1;
1210 let g = this.param.plot[index];
1212 g.marker.lineColor = defaultGraphColor[index];
1213 g.marker.fillColor = defaultGraphColor[index];
1214 g.line.color = defaultGraphColor[index];
1216 // merge optional paramters
1218 deepMerge(g, plotParam);
1220 if (plotParam.xData || plotParam.yData || plotParam.zData)
1221 this.setData(index, plotParam.xData, plotParam.yData, plotParam.zData);
1229 if (typeof label === "string") {
1230 for (const [i,p] of this.param.plot.entries()) {
1231 if (p.label === label) {
1237 alert("Plot \"" + label + "\" not found");
1243 if (index < 0 || index > this.param.plot.length) {
1244 alert("Invalid index \"" + index + "\"");
1251 /** Modify an exising plot
1252 * Plot drawing info is stored in the MPlotGraph.param.plot array. Parameters
1253 * can be modified in realtime by editing this array directly
1254 * @param {int/string} label to the plot array [0...n-1] or label name of plot
1255 * @param {Object} plotParam subset of keys from the {@link defaultPlotParam} object
1256 * @example plt.modifyPlot(0, {label:"Modified plot", line: {color: "red"}} );
1258 modifyPlot(label, plotParam) {
1259 let index = this.findPlot(label);
1260 deepMerge(this.param.plot[index], plotParam);
1263 /** Delete an exising plot
1264 * @param {int/string} label to the plot array [0...n-1] or label name of plot
1265 * @example plt.deletePlot(0);
1268 let index = this.findPlot(label);
1269 this.param.plot.splice(index, 1);
1272 /** Make a download selection menu */
1273 createDownloadSelector () {
1275 // download selector
1276 let downloadSelId = this.parentDiv.id + "downloadSel";
1277 if (document.getElementById(downloadSelId)) document.getElementById(downloadSelId).remove();
1278 this.downloadSelector = document.createElement("div");
1279 this.downloadSelector.id = downloadSelId;
1280 this.downloadSelector.style.display = "none";
1281 this.downloadSelector.style.position = "absolute";
1282 this.downloadSelector.style.backgroundColor = "#FFFFFF";
1283 this.downloadSelector.style.borderRadius = "0";
1284 this.downloadSelector.style.border = "2px solid #808080";
1285 this.downloadSelector.style.margin = "0";
1286 this.downloadSelector.style.padding = "0";
1288 this.downloadSelector.style.left = "100px";
1289 this.downloadSelector.style.top = "100px";
1291 let table = document.createElement("table");
1294 let row = document.createElement("tr");
1295 let cell = document.createElement("td");
1296 cell.style.padding = "0";
1297 let link = document.createElement("a");
1299 link.innerHTML = "CSV";
1300 link.title = "Download data in Comma Separated Value format";
1301 link.onclick = function () {
1302 mhg.downloadSelector.style.display = "none";
1303 mhg.download("CSV");
1306 cell.appendChild(link);
1307 row.appendChild(cell);
1308 table.appendChild(row);
1310 row = document.createElement("tr");
1311 cell = document.createElement("td");
1312 cell.style.padding = "0";
1313 link = document.createElement("a");
1315 link.innerHTML = "PNG";
1316 link.title = "Download image in PNG format";
1317 link.onclick = function () {
1318 mhg.downloadSelector.style.display = "none";
1319 mhg.download("PNG");
1322 cell.appendChild(link);
1323 row.appendChild(cell);
1324 table.appendChild(row);
1326 this.downloadSelector.appendChild(table);
1327 document.body.appendChild(this.downloadSelector);
1330 /** Handle key events
1331 * @param {Object} e keydown event with properties key, metaKey, ctrlKey, target, etc
1334 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
1336 // don't grab key if we are in an input field
1337 if (e.target.tagName === "INPUT")
1345 /** Set the data for the mplot
1346 * For new applications please use addPlot
1347 * @param {int} index Choose which graph to modify 0...n-1. Stored in the MPlotGraph.param.plot array
1348 * @param {float[]} x X values
1349 * @param {float[]} y Y values
1350 * @param {float[]} z Z values
1352 setData(index, x, y, z) {
1354 if (index > this.param.plot.length) {
1355 dlgAlert("Wrong index \"" + index + "\" for graph \""+ this.param.title.text +"\"<br />" +
1356 "New index must be \"" + this.param.plot.length + "\"");
1360 let g = this.param.plot[index];
1362 g.odbPath = ""; // prevent loading of ODB data
1365 if (!checkPlotType(g.type))
1366 throw new Error(`mplot.js: Unknown plot type "${g.type}"`);
1368 if (g.type === "histogram") {
1369 g.line.color = "#404040";
1370 g.marker.draw = false;
1371 // generate X data for histograms
1372 if (g.xMin === undefined || g.xMax === undefined) {
1374 g.xMax = g.yData.length - 0.5;
1376 let dx = (g.xMax - g.xMin) / g.yData.length;
1377 let x0 = g.xMin + dx/2;
1378 if (g.xData === undefined || g.xData === null || g.xData.length === 0)
1379 g.xData = Array.from({length: g.yData.length}, (v,i) => x0 + i*dx);
1383 g.yMin = Math.min(...g.yData);
1384 g.yMax = Math.max(...g.yData);
1387 else if (g.type === "bar") {
1394 g.yMax = Math.max(...g.yData);
1397 else if (g.type === "scatter" ) {
1400 g.xMin = Math.min(...g.xData);
1401 g.xMax = Math.max(...g.xData);
1402 g.yMin = Math.min(...g.yData);
1403 g.yMax = Math.max(...g.yData);
1406 else if (g.type === "colormap") {
1408 g.zMin = Math.min(...g.zData.filter(v=>!isNaN(v)));
1409 g.zMax = Math.max(...g.zData.filter(v=>!isNaN(v)));
1411 if (g.xMin === undefined) {
1413 g.xMax = g.nx - 0.5;
1415 if (g.yMin === undefined) {
1417 g.yMax = g.ny - 0.5;
1420 let dx = (g.xMax - g.xMin) / g.nx;
1421 let x0 = g.xMin + dx/2;
1422 g.xData = Array.from({length: g.nx}, (v,i) => x0 + i*dx);
1424 let dy = (g.yMax - g.yMin) / g.ny;
1425 let y0 = g.yMin + dy/2;
1426 g.yData = Array.from({length: g.ny}, (v,i) => y0 + i*dy);
1429 if (!this.blockAutoScale) {
1436 /** Resize canvas and redraw */
1438 this.canvas.width = this.parentDiv.clientWidth;
1439 this.canvas.height = this.parentDiv.clientHeight;
1443 /** Redraw the graph */
1445 let f = this.draw.bind(this);
1446 window.requestAnimationFrame(f);
1449 /** Convert data coordinates to screen coordinates x axis */
1452 if (this.param.xAxis.log) {
1456 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
1457 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
1459 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
1462 /** Convert data coordinates to screen coordinates y axis */
1464 if (this.param.yAxis.log) {
1468 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
1469 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
1471 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
1474 /** Convert screen coordinates to data coordinates x axis */
1476 if (this.param.xAxis.log) {
1477 let xl = (x - this.x1) / (this.x2 - this.x1) * (Math.log(this.xMax)-Math.log(this.xMin)) + Math.log(this.xMin);
1478 return Math.exp(xl);
1480 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
1483 /** Convert screen coordinates to data coordinates y axis */
1485 if (this.param.yAxis.log) {
1486 let yl = (this.y1 - y) / (this.y1 - this.y2) * (Math.log(this.yMax)-Math.log(this.yMin)) + Math.log(this.yMin);
1487 return Math.exp(yl);
1489 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
1492 /** Calculate the min/max of each axis */
1495 if (this.param.plot.length === 0)
1498 // simple nx / ny for colormaps
1499 if (this.param.plot[0].type === "colormap") {
1500 this.nx = this.param.plot[0].nx;
1501 this.ny = this.param.plot[0].ny;
1503 if (this.param.zAxis.min !== undefined)
1504 this.zMin = this.param.zAxis.min;
1506 this.zMin = this.param.plot[0].zMin;
1508 if (this.param.zAxis.max !== undefined)
1509 this.zMax = this.param.zAxis.max;
1511 this.zMax = this.param.plot[0].zMax;
1513 if (this.param.zAxis.log) {
1514 if (this.zMin < 1E-20)
1516 if (this.zMax < 1E-18)
1520 this.xMin = this.param.plot[0].xMin;
1521 this.xMax = this.param.plot[0].xMax;
1522 this.yMin = this.param.plot[0].yMin;
1523 this.yMax = this.param.plot[0].yMax;
1525 this.xMin0 = this.xMin;
1526 this.xMax0 = this.xMax;
1527 this.yMin0 = this.yMin;
1528 this.yMax0 = this.yMax;
1532 // determine min/max of overall plot
1533 let xMin = this.param.plot[0].xMin;
1534 for (const g of this.param.plot)
1537 if (this.param.xAxis.min !== undefined)
1538 xMin = this.param.xAxis.min;
1540 let xMax = this.param.plot[0].xMax;
1541 for (const g of this.param.plot)
1544 if (this.param.xAxis.max !== undefined)
1545 xMax = this.param.xAxis.max;
1547 let yMin = this.param.plot[0].yMin;
1548 for (const g of this.param.plot)
1551 if (this.param.yAxis.min !== undefined)
1552 yMin = this.param.yAxis.min;
1554 let yMax = this.param.plot[0].yMax;
1555 for (const g of this.param.plot)
1558 if (this.param.yAxis.max !== undefined)
1559 yMax = this.param.yAxis.max;
1561 // avoid min === max
1562 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
1563 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
1565 // add 5% on each side
1566 let dx = (xMax - xMin);
1567 let dy = (yMax - yMin);
1568 if (this.param.plot[0].type !== "histogram" && this.param.plot[0].type !== "bar") {
1569 if (this.param.xAxis.min === undefined)
1571 if (this.param.xAxis.max === undefined)
1573 if (this.param.yAxis.min === undefined)
1576 if (this.param.yAxis.max === undefined)
1590 /** Calculate the stats to display in the graph
1591 * These are stored in MPlotGraph.param.stats. See also defaultGraphParam
1595 let g = this.param.plot[0];
1597 if (g.type === "scatter") {
1598 this.stats.name = ["Entries", "Mean X", "Std Dev X", "Mean Y", "Std Dev Y"];
1600 this.stats.value = Array(5).fill(0);
1601 let n = this.param.plot[0].xData.length;
1604 let mean = g.xData.reduce((sum, x) => sum + x, 0) / n;
1605 let variance = g.xData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1606 let stddev = Math.sqrt(variance);
1607 this.stats.value[0] = n;
1608 this.stats.value[1] = Number(mean.toPrecision(6));
1609 this.stats.value[2] = Number(stddev.toPrecision(6));
1611 mean = g.yData.reduce((sum, x) => sum + x, 0) / n;
1612 variance = g.yData.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
1613 stddev = Math.sqrt(variance);
1614 this.stats.value[3] = Number(mean.toPrecision(6));
1615 this.stats.value[4] = Number(stddev.toPrecision(6));
1619 if (g.type === "histogram") {
1620 this.stats.name = ["Entries", "Mean", "Std Dev"];
1621 this.stats.value = Array(3).fill(0);
1622 let n = g.yData.reduce((sum, y) => sum + y, 0);
1629 for (let i=0 ; i< g.xData.length ; i++) {
1631 sumXY += g.xData[i] * g.yData[i];
1632 sumX2Y += g.xData[i] * g.xData[i] * g.yData[i];
1635 let mean = sumXY / sumY;
1636 let variance = sumX2Y / sumY - mean ** 2;
1637 let stddev = Math.sqrt(variance);
1638 this.stats.value[0] = Number(n);
1639 this.stats.value[1] = Number(mean.toPrecision(6));
1640 this.stats.value[2] = Number(stddev.toPrecision(6));
1644 if (g.type === "bar") {
1645 if (g.yData.length > 0) {
1646 this.stats.name = ["Mean", "Min", "Max"];
1647 this.stats.value = Array(3).fill(0);
1648 let mean = g.yData.reduce((sum, y) => sum + y, 0) / g.yData.length;
1650 this.stats.value[0] = mean.toPrecision(6);
1651 this.stats.value[1] = Math.min(...g.yData).toPrecision(6);
1652 this.stats.value[2] = Math.max(...g.yData).toPrecision(6);
1656 if (g.type === "colormap") {
1657 this.stats.name = ["Entries", "Mean X", "Mean Y", "Std Dev X", "Std Dev Y"];
1659 this.stats.value = Array(5).fill(0);
1661 if (g.nx > 1 && g.ny > 1) {
1664 // calculate x/y values
1665 let dx = (g.xMax - g.xMin) / this.nx;
1666 let dy = (g.yMax - g.yMin) / this.ny;
1668 let xi = Array.from({ length: g.nx }, (_, i) =>
1669 g.xMin + (i + 0.5) * dx);
1671 // sum up all columns projected to X-axis
1672 let sumH = Array(g.nx).fill(0);
1673 for (let i=0 ; i<g.nx ; i++) {
1674 for (let j = 0; j < g.ny; j++) {
1675 n += g.zData[i + j * g.nx];
1676 sumH[i] += g.zData[i + j * g.nx];
1684 for (let i=0 ; i< g.nx ; i++) {
1686 sumXY += xi[i] * sumH[i];
1687 sumX2Y += xi[i] * xi[i] * sumH[i];
1690 let mean = sumXY / sumY;
1691 let variance = sumX2Y / sumY - mean ** 2;
1692 let stddev = Math.sqrt(variance);
1693 this.stats.value[0] = Number(n.toPrecision(6));
1694 this.stats.value[1] = Number(mean.toPrecision(6));
1695 this.stats.value[3] = Number(stddev.toPrecision(6));
1697 //----------------------------------------------
1699 xi = Array.from({ length: g.ny }, (_, i) =>
1700 g.yMin + (i + 0.5) * dy);
1702 // sup up all rows projected to Y-axis
1703 sumH = Array(g.ny).fill(0);
1704 for (let i=0 ; i<g.ny ; i++) {
1705 for (let j = 0; j < g.nx; j++) {
1706 sumH[i] += g.zData[j + i * g.nx];
1714 for (let i=0 ; i< g.ny ; i++) {
1716 sumXY += xi[i] * sumH[i];
1717 sumX2Y += xi[i] * xi[i] * sumH[i];
1720 mean = sumXY / sumY;
1721 variance = sumX2Y / sumY - mean ** 2;
1722 stddev = Math.sqrt(variance);
1723 this.stats.value[2] = Number(mean.toPrecision(6));
1724 this.stats.value[4] = Number(stddev.toPrecision(6));
1730 /** Draw a single marker on plot
1731 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1732 * @param {object} p param object from MPlotGraph
1733 * @param {number} x x coord of marker
1734 * @param {number} y y coord of marker
1736 drawMarker(ctx, p, x, y) {
1737 ctx.strokeStyle = p.marker.lineColor;
1738 ctx.fillStyle = p.marker.fillColor;
1740 let size = p.marker.size;
1741 ctx.lineWidth = p.marker.lineWidth;
1743 switch(p.marker.style) {
1746 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
1751 ctx.fillRect(x - size / 2, y - size / 2, size, size);
1752 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
1756 ctx.moveTo(x, y - size / 2);
1757 ctx.lineTo(x + size / 2, y);
1758 ctx.lineTo(x, y + size / 2);
1759 ctx.lineTo(x - size / 2, y);
1760 ctx.lineTo(x, y - size / 2);
1766 ctx.moveTo(x + size * 0.00, y - size * 0.50);
1767 ctx.lineTo(x + size * 0.48, y - size * 0.16);
1768 ctx.lineTo(x + size * 0.30, y + size * 0.41);
1769 ctx.lineTo(x - size * 0.30, y + size * 0.41);
1770 ctx.lineTo(x - size * 0.48, y - size * 0.16);
1771 ctx.lineTo(x + size * 0.00, y - size * 0.50);
1777 ctx.moveTo(x, y - size / 2);
1778 ctx.lineTo(x + size / 2, y + size / 2);
1779 ctx.lineTo(x - size / 2, y + size / 2);
1780 ctx.lineTo(x, y - size / 2);
1784 case "triangle-down":
1786 ctx.moveTo(x, y + size / 2);
1787 ctx.lineTo(x + size / 2, y - size / 2);
1788 ctx.lineTo(x - size / 2, y - size / 2);
1789 ctx.lineTo(x, y + size / 2);
1793 case "triangle-left":
1795 ctx.moveTo(x - size / 2, y);
1796 ctx.lineTo(x + size / 2, y - size / 2);
1797 ctx.lineTo(x + size / 2, y + size / 2);
1798 ctx.lineTo(x - size / 2, y);
1802 case "triangle-right":
1804 ctx.moveTo(x + size / 2, y);
1805 ctx.lineTo(x - size / 2, y - size / 2);
1806 ctx.lineTo(x - size / 2, y + size / 2);
1807 ctx.lineTo(x + size / 2, y);
1813 ctx.moveTo(x - size / 2, y - size / 2);
1814 ctx.lineTo(x + size / 2, y + size / 2);
1815 ctx.moveTo(x - size / 2, y + size / 2);
1816 ctx.lineTo(x + size / 2, y - size / 2);
1821 ctx.moveTo(x - size / 2, y);
1822 ctx.lineTo(x + size / 2, y);
1823 ctx.moveTo(x, y + size / 2);
1824 ctx.lineTo(x, y - size / 2);
1830 /** Draw a single horizontal errorbar on the plot
1831 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1832 * @param {object} p param object from MPlotGraph
1833 * @param {number} x x coord of bar center (unused)
1834 * @param {number} y y coord of bar center
1835 * @param {number} x1 position of the low side error bar enpoint
1836 * @param {number} x2 position of the high side error bar enpoint
1838 drawXErrorBar(ctx, p, x, y, x1, x2) {
1839 let size = p.marker.size / 2;
1844 ctx.moveTo(x1, y-size);
1845 ctx.lineTo(x1, y+size);
1846 ctx.moveTo(x2, y-size);
1847 ctx.lineTo(x2, y+size);
1851 /** Draw a single vertical errorbar on the plot
1852 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
1853 * @param {object} p param object from MPlotGraph
1854 * @param {number} x x coord of bar center
1855 * @param {number} y y coord of bar center (unused)
1856 * @param {number} y1 position of the low side error bar enpoint
1857 * @param {number} y2 position of the high side error bar enpoint
1859 drawYErrorBar(ctx, p, x, y, y1, y2) {
1860 let size = p.marker.size / 2;
1865 ctx.moveTo(x-size, y1);
1866 ctx.lineTo(x+size, y1);
1867 ctx.moveTo(x-size, y2);
1868 ctx.lineTo(x+size, y2);
1872 /** draw all elements of the graph into the canvas: axes, plots, text, buttons, etc. */
1875 if (!this.canvas || this.param.plot.length === 0)
1878 let ctx = this.canvas.getContext("2d");
1880 this.width = this.canvas.width;
1881 this.height = this.canvas.height;
1883 ctx.fillStyle = this.param.color.background;
1884 ctx.fillRect(0, 0, this.width, this.height);
1886 if (this.error !== null) {
1888 ctx.font = "14px sans-serif";
1889 ctx.strokeStyle = "#808080";
1890 ctx.fillStyle = "#808080";
1891 ctx.textAlign = "center";
1892 ctx.textBaseline = "middle";
1893 ctx.fillText(this.error, this.width / 2, this.height / 2);
1897 if (this.param.plot[0].xData === undefined) {
1899 ctx.font = "14px sans-serif";
1900 ctx.strokeStyle = "#808080";
1901 ctx.fillStyle = "#808080";
1902 ctx.textAlign = "center";
1903 ctx.textBaseline = "middle";
1904 ctx.fillText("No data-odb-path present and no setData() called", this.width / 2, this.height / 2);
1908 if (this.height === undefined || this.width === undefined)
1910 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1913 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1915 let axisLabelWidth = this.drawYAxis(ctx, 50, this.height - 25, this.height - 35,
1916 -4, -7, -10, -12, 0, this.yMin, this.yMax, this.param.yAxis.log, false);
1918 if (axisLabelWidth === undefined)
1921 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1922 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1924 this.x1 = axisLabelWidth + 15;
1926 this.x2 = this.param.showMenuButtons ? this.width - 30 : this.width - 2;
1927 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "")
1928 this.x2 -= 1.0*this.param.zAxis.title.textSize;
1930 if (this.param.showMenuButtons === false)
1931 this.x2 = this.width - 2;
1933 this.y1 = this.height;
1936 let axisLabelHeight;
1937 if (this.param.xAxis.type === "category")
1938 axisLabelHeight = this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
1939 10, 12, this.xMin, this.xMax, this.param.plot[0].xData, false);
1941 axisLabelHeight = this.param.xAxis.textSize;
1943 axisLabelHeight += 12; // space for ticks and frame
1945 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "")
1946 this.y1 = this.height - axisLabelHeight - 1.5*this.param.xAxis.title.textSize;
1948 this.y1 = this.height - axisLabelHeight;
1950 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1951 if (this.zMin === undefined || this.zMax === undefined) {
1955 if (this.zMin === this.zMax) {
1960 ctx.font = this.param.zAxis.textSize + "px sans-serif";
1961 axisLabelWidth = this.drawYAxis(ctx, this.x2 + 30, this.y1, this.y1 - this.y2,
1962 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, false);
1963 if (axisLabelWidth === undefined)
1966 if (this.param.zAxis.show) {
1967 let w = 5; // left gap
1968 w += 10; // color bar
1969 w += 12; // tick width
1972 this.x2 -= axisLabelWidth + w;
1973 this.param.zAxis.width = axisLabelWidth + w;
1978 if (this.param.title.text !== "") {
1979 ctx.strokeStyle = this.param.color.axis;
1980 ctx.fillStyle = "#F0F0F0";
1981 ctx.font = this.param.title.textSize + "px sans-serif";
1982 let h = this.param.title.textSize * 1.2;
1983 ctx.strokeRect(this.x1, 6, this.x2 - this.x1, h);
1984 ctx.fillRect(this.x1, 6, this.x2 - this.x1, h);
1985 ctx.textAlign = "center";
1986 ctx.textBaseline = "middle";
1987 ctx.fillStyle = this.param.title.color;
1988 ctx.fillText(this.param.title.text, (this.x2 + this.x1) / 2, 6 + h/2);
1993 ctx.strokeStyle = this.param.color.axis;
1995 if (this.param.yAxis.log && this.yMin < 1E-20)
1997 if (this.param.yAxis.log && this.yMax < 1E-18)
2000 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
2002 ctx.fillStyle = this.param.title.color;
2003 let s = this.param.xAxis.title.textSize;
2004 ctx.font = s + "px sans-serif";
2005 ctx.textAlign = "center";
2006 ctx.textBaseline = "top";
2007 ctx.fillText(this.param.xAxis.title.text, (this.x1 + this.x2)/2,
2008 this.y1 + this.param.xAxis.textSize + 10 + this.param.xAxis.title.textSize / 4);
2012 ctx.font = this.param.xAxis.textSize + "px sans-serif";
2013 let grid = this.param.xAxis.grid ? this.y2 - this.y1 : 0;
2015 if (this.param.xAxis.type === "numeric")
2016 this.drawXAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
2017 4, 7, 10, 10, grid, this.xMin, this.xMax, this.param.xAxis.log);
2018 else if (this.param.xAxis.type === "datetime")
2019 this.drawTAxis(ctx, this.x1, this.y1, this.x2 - this.x1, this.width,
2020 4, 7, 10, 10, grid, this.xMin, this.xMax);
2021 else if (this.param.xAxis.type === "category")
2022 this.drawCAxis(ctx, this.x1, this.y1, this.x2 - this.x1,
2023 10, 12, this.xMin, this.xMax, this.param.plot[0].xData, true);
2025 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
2027 ctx.fillStyle = this.param.title.color;
2028 let s = this.param.yAxis.title.textSize;
2029 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
2030 ctx.rotate(-Math.PI / 2);
2031 ctx.font = s + "px sans-serif";
2032 ctx.textAlign = "center";
2033 ctx.textBaseline = "top";
2034 ctx.fillText(this.param.yAxis.title.text, 0, 0);
2038 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
2040 ctx.fillStyle = this.param.title.color;
2041 let s = this.param.zAxis.title.textSize;
2042 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
2043 ctx.rotate(-Math.PI / 2);
2044 ctx.font = s + "px sans-serif";
2045 ctx.textAlign = "center";
2046 ctx.textBaseline = "middle";
2047 ctx.fillText(this.param.zAxis.title.text, 0, this.x2 + this.param.zAxis.width);
2051 ctx.font = this.param.yAxis.textSize + "px sans-serif";
2052 grid = this.param.yAxis.grid ? this.x2 - this.x1 : 0;
2053 this.drawYAxis(ctx, this.x1, this.y1, this.y1 - this.y2,
2054 -4, -7, -10, -12, grid, this.yMin, this.yMax, this.param.yAxis.log, true);
2056 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
2058 let s = this.param.yAxis.title.textSize;
2059 ctx.translate(s / 2, (this.y1 + this.y2) / 2);
2060 ctx.rotate(-Math.PI / 2);
2061 ctx.font = s + "px sans-serif";
2062 ctx.textAlign = "center";
2063 ctx.textBaseline = "top";
2064 ctx.fillText(this.param.yAxis.title.text, 0, 0);
2069 ctx.strokeStyle = this.param.color.axis;
2070 ctx.strokeRect(this.x1, this.y1, this.x2-this.x1, this.y2-this.y1);
2072 // set clipping region not to draw outside axes
2074 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2079 for (const [plotIndex,p] of this.param.plot.entries()) {
2080 if (p.xData === undefined || p.xData === null)
2083 if (p.xData.length > 0)
2086 ctx.globalAlpha = p.alpha;
2088 if (p.type === "scatter") {
2090 if (p.line && p.line.draw ||
2091 p.line && p.line.fill) {
2093 ctx.fillStyle = p.line.color;
2094 ctx.strokeStyle = ctx.fillStyle;
2098 ctx.globalAlpha = 0.1;
2100 ctx.moveTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
2101 for (let i = 0; i < p.xData.length; i++) {
2102 let x = this.xToScreen(p.xData[i]);
2103 let y = this.yToScreen(p.yData[i]);
2106 ctx.lineTo(this.xToScreen(p.xData[p.xData.length - 1]), this.yToScreen(0));
2107 ctx.lineTo(this.xToScreen(p.xData[0]), this.yToScreen(0));
2109 ctx.globalAlpha = 1;
2113 if (p.line.draw && p.line.width > 0 && p.line.style !== "none") {
2114 ctx.lineWidth = p.line.width;
2116 if (p.line.style === "dashed")
2117 ctx.setLineDash([5,5]);
2118 if (p.line.style === "dotted")
2119 ctx.setLineDash([1,10]);
2120 for (let i = 0; i < p.xData.length; i++) {
2121 let x = this.xToScreen(p.xData[i]);
2122 let y = this.yToScreen(p.yData[i]);
2129 ctx.setLineDash([]);
2134 if (p.marker && p.marker.draw) {
2135 for (let i = 0; i < p.xData.length; i++) {
2137 let x = this.xToScreen(p.xData[i]);
2138 let y = this.yToScreen(p.yData[i]);
2140 this.drawMarker(ctx, p, x, y);
2143 let x1 = this.xToScreen(p.xData[i]-p.xErrorData[i]);
2144 let x2 = this.xToScreen(p.xData[i]+p.xErrorData[i]);
2145 this.drawXErrorBar(ctx, p, x, y, x1, x2);
2149 let y1 = this.yToScreen(p.yData[i]+p.yErrorData[i]);
2150 let y2 = this.yToScreen(p.yData[i]-p.yErrorData[i]);
2151 this.drawYErrorBar(ctx, p, x, y, y1, y2);
2157 else if (p.type === "histogram") {
2159 let dx = (p.xMax - p.xMin) / p.xData.length;
2160 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
2167 ctx.fillStyle = p.line.color;
2168 ctx.strokeStyle = ctx.fillStyle;
2171 ctx.moveTo(this.xToScreen(p.xData[0])-dxs/2, this.yToScreen(0));
2172 for (let i = 0; i < p.xData.length; i++) {
2173 x = this.xToScreen(p.xData[i]);
2174 y = this.yToScreen(p.yData[i]);
2175 ctx.lineTo(x-dxs/2, y);
2176 ctx.lineTo(x+dxs/2, y);
2178 ctx.lineTo(x+dxs/2, this.yToScreen(0));
2179 ctx.globalAlpha = 0.2;
2181 ctx.globalAlpha = 1;
2185 else if (p.type === "bar") {
2187 let dx = (this.x2 - this. x1) * (this.xMax0-this.xMin0) / (this.xMax-this.xMin) / p.xData.length;
2190 if (this.param.barWidth)
2191 totalWidth = dx * this.param.barWidth;
2193 totalWidth = dx * 0.3;
2195 let barWidth = totalWidth / this.param.plot.length; // each plot gets a fraction of the bar width
2196 let offset = plotIndex * barWidth;
2198 if (p.xData.length < 100)
2203 ctx.fillStyle = p.line.color;
2204 ctx.strokeStyle = ctx.fillStyle;
2207 for (let i = 0; i < p.xData.length; i++) {
2208 x = this.xToScreen(i + 0.5);
2209 y = this.yToScreen(p.yData[i]);
2210 ctx.moveTo(x-totalWidth/2+offset, this.yToScreen(0));
2211 ctx.lineTo(x-totalWidth/2+offset, y);
2212 ctx.lineTo(x-totalWidth/2+barWidth+offset, y);
2213 ctx.lineTo(x-totalWidth/2+barWidth+offset, this.yToScreen(0));
2214 ctx.lineTo(x-totalWidth/2+offset, this.yToScreen(0));
2216 ctx.globalAlpha = 0.2;
2218 ctx.globalAlpha = 1;
2222 else if (p.type === "colormap") {
2223 let dx = (p.xMax - p.xMin) / this.nx;
2224 let dy = (p.yMax - p.yMin) / this.ny;
2226 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
2227 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
2229 for (let i=0 ; i<p.ny ; i++) {
2230 for (let j=0 ; j<p.nx ; j++) {
2231 let x = this.xToScreen(j * dx + p.xMin);
2232 let y = this.yToScreen(i * dy + p.yMin);
2233 let zval = this.param.plot[0].zData[j+i*p.nx];
2235 ctx.fillStyle = 'hsl(255, 0%, 50%)';
2238 if (this.param.zAxis.log) {
2242 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
2244 v = (zval - this.zMin) / (this.zMax - this.zMin);
2252 if (zval < 0.5 && this.param.plot[0].zeroColor)
2253 ctx.fillStyle = this.param.plot[0].zeroColor;
2255 ctx.fillStyle = colorPalette[Math.floor((v)*255)];
2257 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
2264 ctx.restore(); // remove clipping
2267 if (this.param.plot[0].type === "colormap") {
2268 if (this.param.plot[0].showZScale) {
2270 for (let i=0 ; i<100 ; i++) {
2272 ctx.fillStyle = colorPalette[Math.floor((1-v)*255)];
2273 ctx.fillRect(this.x2 + 5, this.y2 + i/100*(this.y1 - this.y2),
2274 10, (this.y1 - this.y2) / 100 + 1);
2278 ctx.strokeStyle = this.param.color.axis;
2280 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
2283 ctx.font = this.param.zAxis.textSize + "px sans-serif";
2284 ctx.strokeStyle = this.param.color.axis;
2286 this.drawYAxis(ctx, this.x2 + 15, this.y1, this.y1 - this.y2,
2287 4, 7, 10, 12, 0, this.zMin, this.zMax, this.param.zAxis.log, true);
2293 for (const g of this.param.plot)
2294 if (g.label && g.label !== "")
2297 if (this.param.legend?.show && nLabel > 0) {
2298 ctx.font = this.param.legend.textSize + "px sans-serif";
2301 for (const g of this.param.plot) {
2302 if (ctx.measureText(g.label).width > mw) {
2303 mw = ctx.measureText(g.label).width;
2306 let w = 50 + mw + 5;
2307 let h = this.param.legend.textSize * 1.5;
2309 ctx.fillStyle = this.param.legend.backgroundColor;
2310 ctx.strokeStyle = this.param.legend.color;
2311 ctx.fillRect(this.x1, this.y2, w, h * this.param.plot.length);
2312 ctx.strokeRect(this.x1, this.y2, w, h * this.param.plot.length);
2314 for (const [gi,g] of this.param.plot.entries()) {
2315 if (g.line && g.line.draw && g.line.width > 0 && g.line.style !== "none") {
2317 ctx.strokeStyle = g.line.color;
2318 ctx.lineWidth = g.line.width;
2320 if (g.line.style === "dashed")
2321 ctx.setLineDash([5,5]);
2322 if (g.line.style === "dotted")
2323 ctx.setLineDash([1,10]);
2326 ctx.moveTo(this.x1 + 5, this.y2 + gi*h + h/2);
2327 ctx.lineTo(this.x1 + 35, this.y2 + gi*h + h/2);
2330 ctx.setLineDash([]);
2332 ctx.strokeStyle = g.line.color;
2333 ctx.lineWidth = g.line.width;
2335 this.drawMarker(ctx, g, this.x1 + 20, this.y2 + gi*h + h/2);
2336 ctx.textAlign = "left";
2337 ctx.textBaseline = "middle";
2338 ctx.fillStyle = this.param.color.axis;
2339 ctx.fillText(g.label, this.x1 + 40, this.y2 + gi*h + h/2);
2346 if (this.param.stats.show && this.stats.name) {
2347 ctx.font = this.param.legend.textSize + "px sans-serif";
2350 for (const [si,s] of this.stats.name.entries()) {
2351 let str = s + " " + this.stats.value[si].toString();
2352 if (ctx.measureText(str).width > mw) {
2353 mw = ctx.measureText(str).width;
2357 let h = this.param.legend.textSize * 1.5;
2359 ctx.fillStyle = this.param.legend.backgroundColor;
2360 ctx.strokeStyle = this.param.legend.color;
2361 ctx.fillRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
2362 ctx.strokeRect(this.x2 - w, this.y2, w, h * this.stats.name.length);
2364 for (const [si,s] of this.stats.name.entries()) {
2365 ctx.textAlign = "left";
2366 ctx.textBaseline = "middle";
2367 ctx.fillStyle = this.param.color.axis;
2368 ctx.fillText(s, this.x2 - w + 5, this.y2 + si*h + h/2);
2369 ctx.textAlign = "right";
2370 let str = this.stats.value[si].toString();
2371 ctx.fillText(str, this.x2 - 5, this.y2 + si*h + h/2);
2375 // "empty window" notice
2377 ctx.font = "16px sans-serif";
2378 let str = "No data available";
2379 ctx.strokeStyle = "#404040";
2380 ctx.fillStyle = "#F0F0F0";
2381 let w = ctx.measureText(str).width + 10;
2383 ctx.fillRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
2384 ctx.strokeRect((this.x1 + this.x2) / 2 - w / 2, (this.y1 + this.y2) / 2 - h / 2, w, h);
2385 ctx.fillStyle = "#404040";
2386 ctx.textAlign = "center";
2387 ctx.textBaseline = "middle";
2388 ctx.fillText(str, (this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
2389 ctx.font = "14px sans-serif";
2393 if (this.param.showMenuButtons) {
2395 let buttonSize = 20;
2396 this.button.forEach(b => {
2398 b.x1 = this.width - buttonSize - 6;
2399 b.y1 = 6 + y * (buttonSize + 4);
2400 b.width = buttonSize + 4;
2401 b.height = buttonSize + 4;
2404 ctx.fillStyle = "#F0F0F0";
2405 ctx.strokeStyle = "#808080";
2406 ctx.fillRect(b.x1, b.y1, b.width, b.height);
2407 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
2408 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
2415 if (this.zoom.x.active) {
2416 ctx.fillStyle = "#808080";
2417 ctx.globalAlpha = 0.2;
2418 ctx.fillRect(this.zoom.x.x1, this.y2, this.zoom.x.x2 - this.zoom.x.x1, this.y1 - this.y2);
2419 ctx.globalAlpha = 1;
2420 ctx.strokeStyle = "#808080";
2421 ctx.drawLine(this.zoom.x.x1, this.y1, this.zoom.x.x1, this.y2);
2422 ctx.drawLine(this.zoom.x.x2, this.y1, this.zoom.x.x2, this.y2);
2424 if (this.zoom.y.active) {
2425 ctx.fillStyle = "#808080";
2426 ctx.globalAlpha = 0.2;
2427 ctx.fillRect(this.x1, this.zoom.y.y1, this.x2 - this.x1, this.zoom.y.y2 - this.zoom.y.y1);
2428 ctx.globalAlpha = 1;
2429 ctx.strokeStyle = "#808080";
2430 ctx.drawLine(this.x1, this.zoom.y.y1, this.x2, this.zoom.y.y1);
2431 ctx.drawLine(this.x1, this.zoom.y.y2, this.x2, this.zoom.y.y2);
2435 if (this.marker.active) {
2438 if (this.param.plot[0].type !== "colormap") {
2440 ctx.globalAlpha = 0.1;
2441 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
2442 ctx.fillStyle = "#000000";
2444 ctx.globalAlpha = 1;
2447 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
2448 ctx.fillStyle = "#000000";
2452 ctx.strokeStyle = "#A0A0A0";
2453 ctx.drawLine(this.marker.sx, this.y1, this.marker.sx, this.y2);
2454 ctx.drawLine(this.x1, this.marker.sy, this.x2, this.marker.sy);
2457 ctx.font = "12px sans-serif";
2458 ctx.textAlign = "left";
2460 if (this.parentDiv.dataset.tooltip) {
2461 let f = this.parentDiv.dataset.tooltip;
2462 if (f.indexOf('(') !== -1) // strip any '('
2463 f = f.substring(0, f.indexOf('('));
2465 s = eval(f + "(this)");
2467 if (this.param.plot[0].type === "bar")
2468 s = this.marker.x + " / " +
2469 this.marker.y.toPrecision(6).stripZeros();
2471 s = this.marker.x.toPrecision(6).stripZeros() + " / " +
2472 this.marker.y.toPrecision(6).stripZeros();
2473 if (this.param.plot[0].type === "colormap")
2474 s += ": " + (this.marker.z === null ? "null" : this.marker.z.toPrecision(6).stripZeros());
2476 let w = ctx.measureText(s).width + 6;
2477 let h = ctx.measureText("M").width * 1.2 + 6;
2478 let x = this.marker.mx + 10;
2479 let y = this.marker.my - 20;
2481 // move marker inside if outside plotting area
2482 if (x + w >= this.x2)
2483 x = this.marker.sx - 10 - w;
2485 ctx.strokeStyle = "#808080";
2486 ctx.fillStyle = "#F0F0F0";
2487 ctx.textBaseline = "middle";
2488 ctx.fillRect(x, y, w, h);
2489 ctx.strokeRect(x, y, w, h);
2490 ctx.fillStyle = "#404040";
2491 ctx.fillText(s, x + 3, y + h / 2);
2494 // call optional user overlay function
2495 if (this.param.overlay) {
2498 ctx.textAlign = "left";
2499 ctx.textBaseline = "top";
2500 ctx.fillStyle = "black";
2501 ctx.strokeStyle = "black";
2502 ctx.font = "12px sans-serif";
2504 eval(this.param.overlay + "(this, ctx)");
2510 /** Draw the xaxis as "numeric"
2511 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2512 * @param {number} x1 coordinate position of the axis on screen
2513 * @param {number} y1 coordinate position of the axis on screen
2514 * @param {number} width width of the axis, likely also the width of the plot
2515 * @param {number} minor step between minor ticks
2516 * @param {number} major step between major ticks
2517 * @param {string} text
2518 * @param {number} label distance of labels
2519 * @param {bool} grid if true draw grid lines over minor ticks
2520 * @param {number} xmin low limit of the axis
2521 * @param {number} xmax high limit of the axis
2522 * @param {bool} logaxis if true draw axis on a log scale (base 10)
2524 drawXAxis(ctx, x1, y1, width, minor, major, text, label, grid, xmin, xmax, logaxis) {
2525 var dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
2526 var tick_base, major_base, label_base, n_sig1, n_sig2, xs;
2527 var base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
2529 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
2532 if (xmax <= xmin || width <= 0)
2535 ctx.textAlign = "center";
2536 ctx.textBaseline = "top";
2540 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
2541 if (isNaN(dx) || dx === 0) {
2549 } else { // linear axis ----
2551 // use 10 as min tick distance
2552 dx = (xmax - xmin) / (width / 10);
2554 int_dx = Math.floor(Math.log(dx) / LN10);
2555 frac_dx = Math.log(dx) / LN10 - int_dx;
2562 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
2563 major_base = label_base = tick_base + 1;
2565 // rounding up of dx, label_dx
2566 dx = Math.pow(10, int_dx) * base[tick_base];
2567 major_dx = Math.pow(10, int_dx) * base[major_base];
2568 label_dx = major_dx;
2571 // number of significant digits
2575 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2580 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2582 n_sig1 = Math.max(n_sig1, n_sig2);
2584 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2585 if (Math.abs(xmin) < 100000)
2586 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / LN10) + 1);
2587 if (Math.abs(xmax) < 100000)
2588 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / LN10) + 1);
2590 // determination of maximal width of labels
2591 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
2592 let ext = ctx.measureText(str);
2593 maxwidth = ext.width;
2595 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
2596 ext = ctx.measureText(str);
2597 maxwidth = Math.max(maxwidth, ext.width);
2598 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
2599 maxwidth = Math.max(maxwidth, ext.width);
2601 // increasing label_dx, if labels would overlap
2602 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
2604 label_dx = Math.pow(10, int_dx) * base[label_base];
2605 if (label_base % 3 === 2 && major_base % 3 === 1) {
2607 major_dx = Math.pow(10, int_dx) * base[major_base];
2615 x_act = Math.floor(xmin / dx) * dx;
2617 ctx.strokeStyle = this.param.color.axis;
2618 ctx.drawLine(x1, y1, x1 + width, y1);
2619 let last_label_x = 0;
2623 x_screen = (Math.log(x_act) - Math.log(xmin)) /
2624 (Math.log(xmax) - Math.log(xmin)) * width + x1;
2626 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
2627 xs = Math.floor(x_screen + 0.5);
2629 if (x_screen > x1 + width + 0.001)
2632 if (x_screen >= x1) {
2633 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
2634 dx / major_dx / 10.0) {
2636 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
2637 dx / label_dx / 10.0) {
2639 ctx.strokeStyle = this.param.color.axis;
2640 ctx.drawLine(xs, y1, xs, y1 + text);
2643 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2644 ctx.strokeStyle = this.param.color.grid;
2645 ctx.drawLine(xs, y1, xs, y1 + grid);
2650 let str = x_act.toPrecision(n_sig1).stripZeros();
2651 let ext = ctx.measureText(str);
2652 if (xs - ext.width / 2 > x1 &&
2653 xs + ext.width / 2 < x1 + width) {
2654 ctx.strokeStyle = this.param.color.label;
2655 ctx.fillStyle = this.param.color.label;
2656 ctx.fillText(str, xs, y1 + label);
2658 last_label_x = xs + ext.width / 2;
2662 ctx.strokeStyle = this.param.color.axis;
2663 ctx.drawLine(xs, y1, xs, y1 + major);
2666 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2667 ctx.strokeStyle = this.param.color.grid;
2668 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2680 ctx.strokeStyle = this.param.color.axis;
2681 ctx.drawLine(xs, y1, xs, y1 + minor);
2685 // for log axis, also put grid lines on minor tick marks
2686 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2687 ctx.strokeStyle = this.param.color.grid;
2688 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
2691 // for log axis, also put labels on minor tick marks
2694 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
2695 str = x_act.toExponential(n_sig1).stripZeros();
2697 str = x_act.toPrecision(n_sig1).stripZeros();
2698 let ext = ctx.measureText(str);
2699 if (xs - ext.width / 2 > x1 &&
2700 xs + ext.width / 2 < x1 + width &&
2701 xs - ext.width / 2 > last_label_x + 5) {
2702 ctx.strokeStyle = this.param.color.label;
2703 ctx.fillStyle = this.param.color.label;
2704 ctx.fillText(str, xs, y1 + label);
2707 last_label_x = xs + ext.width / 2;
2714 /* suppress 1.23E-17 ... */
2715 if (Math.abs(x_act) < dx / 100)
2721 /** Draw the yaxis as "numeric"
2722 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2723 * @param {number} x1 coordinate position of the axis on screen
2724 * @param {number} y1 coordinate position of the axis on screen
2725 * @param {number} height height of the axis, likely also the height of the plot
2726 * @param {number} minor step between minor ticks
2727 * @param {number} major step between major ticks
2728 * @param {string} text
2729 * @param {string} label
2730 * @param {bool} grid if true draw grid lines over minor ticks
2731 * @param {number} ymin low limit of the axis
2732 * @param {number} ymax high limit of the axis
2733 * @param {bool} logaxis if true draw axis on a log scale (base 10)
2734 * @param {bool} draw if true draw the axis
2736 drawYAxis(ctx, x1, y1, height, minor, major, text, label, grid, ymin, ymax, logaxis, draw) {
2737 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen;
2738 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
2739 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
2741 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
2744 if (ymax <= ymin || height <= 0)
2748 ctx.textAlign = "right";
2750 ctx.textAlign = "left";
2751 ctx.textBaseline = "middle";
2752 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
2754 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
2755 dy = Number.MAX_VALUE / 10;
2759 } else if (logaxis) {
2760 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
2761 if (isNaN(dy) || dy === 0) {
2769 // use 6 as min tick distance
2770 dy = (ymax - ymin) / (height / 6);
2772 int_dy = Math.floor(Math.log(dy) / Math.log(10));
2773 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
2780 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
2781 major_base = label_base = tick_base + 1;
2783 // rounding up of dy, label_dy
2784 dy = Math.pow(10, int_dy) * base[tick_base];
2785 major_dy = Math.pow(10, int_dy) * base[major_base];
2786 label_dy = major_dy;
2788 // number of significant digits
2792 n_sig1 = Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) -
2793 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2798 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) -
2799 Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
2801 n_sig1 = Math.max(n_sig1, n_sig2);
2802 n_sig1 = Math.max(1, n_sig1);
2804 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase precision to number of digits
2805 if (Math.abs(ymin) < 100000)
2806 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) /
2807 Math.log(10) + 0.001) + 1);
2808 if (Math.abs(ymax) < 100000)
2809 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) /
2810 Math.log(10) + 0.001) + 1);
2812 // increase label_dy if labels would overlap
2813 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
2815 label_dy = Math.pow(10, int_dy) * base[label_base];
2816 if (label_base % 3 === 2 && major_base % 3 === 1) {
2818 major_dy = Math.pow(10, int_dy) * base[major_base];
2823 y_act = Math.floor(ymin / dy) * dy;
2825 let last_label_y = y1;
2829 ctx.strokeStyle = this.param.color.axis;
2830 ctx.drawLine(x1, y1, x1, y1 - height);
2835 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) /
2836 (Math.log(ymax) - Math.log(ymin)) * height;
2837 else if (!(isFinite(ymax - ymin)))
2838 y_screen = y1 - ((y_act/ymin) - 1) / ((ymax/ymin) - 1) * height;
2840 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
2841 ys = Math.round(y_screen);
2843 if (y_screen < y1 - height - 0.001 || isNaN(ys))
2846 if (y_screen <= y1 + 0.001) {
2847 if (Math.abs(Math.round(y_act / major_dy) - y_act / major_dy) <
2848 dy / major_dy / 10.0) {
2850 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
2851 dy / label_dy / 10.0) {
2854 ctx.strokeStyle = this.param.color.axis;
2855 ctx.drawLine(x1, ys, x1 + text, ys);
2859 if (grid !== 0 && ys < y1 && ys > y1 - height)
2861 ctx.strokeStyle = this.param.color.grid;
2862 ctx.drawLine(x1, ys, x1 + grid, ys);
2868 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2869 str = y_act.toExponential(n_sig1).stripZeros();
2871 str = y_act.toPrecision(n_sig1).stripZeros();
2872 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2874 ctx.strokeStyle = this.param.color.label;
2875 ctx.fillStyle = this.param.color.label;
2876 ctx.fillText(str, x1 + label, ys);
2878 last_label_y = ys - textHeight / 2;
2883 ctx.strokeStyle = this.param.color.axis;
2884 ctx.drawLine(x1, ys, x1 + major, ys);
2888 if (grid !== 0 && ys < y1 && ys > y1 - height)
2890 ctx.strokeStyle = this.param.color.grid;
2891 ctx.drawLine(x1, ys, x1 + grid, ys);
2904 ctx.strokeStyle = this.param.color.axis;
2905 ctx.drawLine(x1, ys, x1 + minor, ys);
2911 // for log axis, also put grid lines on minor tick marks
2912 if (grid !== 0 && ys < y1 && ys > y1 - height) {
2914 ctx.strokeStyle = this.param.color.grid;
2915 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
2919 // for log axis, also put labels on minor tick marks
2922 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2923 str = y_act.toExponential(n_sig1).stripZeros();
2925 str = y_act.toPrecision(n_sig1).stripZeros();
2926 if (ys - textHeight / 2 > y1 - height &&
2927 ys + textHeight / 2 < y1 &&
2928 ys + textHeight < last_label_y + 2) {
2929 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2931 ctx.strokeStyle = this.param.color.label;
2932 ctx.fillStyle = this.param.color.label;
2933 ctx.fillText(str, x1 + label, ys);
2944 // suppress 1.23E-17 ...
2945 if (Math.abs(y_act) < dy / 100)
2953 /** Draw the xaxis as "datetime"
2954 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
2955 * @param {number} x1 coordinate position of the axis on screen
2956 * @param {number} y1 coordinate position of the axis on screen
2957 * @param {number} width width of the axis, likely also the width of the plot
2958 * @param {number} xr
2959 * @param {number} minor step between minor ticks
2960 * @param {number} major step between major ticks
2961 * @param {string} text
2962 * @param {string} label
2963 * @param {bool} grid if true draw grid lines over minor ticks
2964 * @param {number} xmin low limit of the axis
2965 * @param {number} xmax high limit of the axis
2967 drawTAxis(ctx, x1, y1, width, xr, minor, major, text, label, grid, xmin, xmax) {
2968 const base = [1, 5, 10, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 3600,
2969 3 * 3600, 6 * 3600, 12 * 3600, 24 * 3600];
2971 ctx.textAlign = "left";
2972 ctx.textBaseline = "top";
2974 if (xmax <= xmin || width <= 0)
2977 /* force date display if xmax not today */
2978 let d1 = new Date(xmax * 1000);
2979 let d2 = new Date();
2980 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
2982 /* use 5 pixel as min tick distance */
2983 let dx = Math.round((xmax - xmin) / (width / 5));
2986 for (tick_base = 0; base[tick_base]; tick_base++) {
2987 if (base[tick_base] > dx)
2990 if (!base[tick_base])
2992 dx = base[tick_base];
2994 let major_base = tick_base;
2997 let label_base = major_base;
3001 let str = ptimeToLabel(xmin, label_dx, forceDate);
3002 let maxWidth = ctx.measureText(str).width;
3004 /* increasing label_dx, if labels would overlap */
3005 if (maxWidth > 0.75 * label_dx / (xmax - xmin) * width) {
3006 if (base[label_base + 1])
3007 label_dx = base[++label_base];
3009 label_dx += 3600 * 24;
3011 if (label_base > major_base + 1 || !base[label_base + 1]) {
3012 if (base[major_base + 1])
3013 major_dx = base[++major_base];
3015 major_dx += 3600 * 24;
3018 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3019 if (base[tick_base + 1])
3020 dx = base[++tick_base];
3029 let d = new Date(xmin * 1000);
3030 let tz = d.getTimezoneOffset() * 60;
3032 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3034 ctx.strokeStyle = this.param.color.axis;
3035 ctx.drawLine(x1, y1, x1 + width, y1);
3038 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3040 if (xs > x1 + width + 0.001)
3044 if ((x_act - tz) % major_dx === 0) {
3045 if ((x_act - tz) % label_dx === 0) {
3047 ctx.strokeStyle = this.param.color.axis;
3048 ctx.drawLine(xs, y1, xs, y1 + text);
3051 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3052 ctx.strokeStyle = this.param.color.grid;
3053 ctx.drawLine(xs, y1, xs, y1 + grid);
3058 let str = ptimeToLabel(x_act, label_dx, forceDate);
3060 // if labels at edge, shift them in
3061 let xl = xs - ctx.measureText(str).width / 2;
3064 if (xl + ctx.measureText(str).width >= xr)
3065 xl = xr - ctx.measureText(str).width - 1;
3066 ctx.strokeStyle = this.param.color.label;
3067 ctx.fillStyle = this.param.color.label;
3068 ctx.fillText(str, xl, y1 + label);
3072 ctx.strokeStyle = this.param.color.axis;
3073 ctx.drawLine(xs, y1, xs, y1 + major);
3077 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3078 ctx.strokeStyle = this.param.color.grid;
3079 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
3083 ctx.strokeStyle = this.param.color.axis;
3084 ctx.drawLine(xs, y1, xs, y1 + minor);
3093 /** Draw the xaxis as "category"
3094 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
3095 * @param {number} x1 coordinate position of the axis on screen
3096 * @param {number} y1 coordinate position of the axis on screen
3097 * @param {number} width width of the axis, likely also the width of the plot
3098 * @param {number} tick height in pixel of tick markers at labels
3099 * @param {number} label distance of text from axis
3100 * @param {string} category array of category labels to plot
3101 * @param {bool} draw if false, only return height of labels
3102 * @returns {float} maximum height + 2
3104 drawCAxis(ctx, x1, y1, width, tick, label, xmin, xmax, category, draw) {
3105 ctx.textBaseline = "middle";
3110 ctx.strokeStyle = this.param.color.axis;
3113 ctx.drawLine(x1, y1, x1 + width, y1);
3115 // spacing between ticks needs to scale with the change in the axis limits
3116 // then rescale to width in screen units
3117 let dx = (this.xMax0 - this.xMin0) / (xmax - xmin) * width / category.length;
3121 let x_offset = this.xMin0 - xmin + 0.5; // offset for tick translation
3123 // determine tick positions
3124 for (angle = 0 ; angle < 90 ; angle += 10) {
3127 for (let i = 0; i < category.length; i++) {
3130 let shift = dx * (i + x_offset);
3131 if (draw && (shift >= 0) && (shift <= width)){
3132 ctx.drawLine(x1 + shift, y1, x1 + shift, y1 + tick);
3136 let w = ctx.measureText(category[i]).width;
3137 let h = this.param.xAxis.textSize;
3139 const cos = Math.cos(angle/180*Math.PI);
3140 const sin = Math.sin(angle/180*Math.PI);
3142 const width2 = Math.abs(w * cos) + Math.abs(h * sin);
3143 const height = Math.abs(w * sin) + Math.abs(h * cos);
3145 maxWidth = Math.max(maxWidth, width2);
3146 maxHeight = Math.max(maxHeight, height);
3149 if (maxWidth * 1.1 < dx)
3153 this.param.xAxis.angle = angle;
3154 var voffset; // vertical offset for text label
3157 // set vertical offset and text alignment based on angle
3160 ctx.textAlign = "right";
3162 var voffset = maxHeight / 2;
3163 ctx.textAlign = "center";
3167 ctx.fillStyle = this.param.color.label; // without this the text color matches the background unless the title is first set
3169 for (let i = 0; i < category.length; i++) {
3171 // check if the tick is within axis limits
3172 let shift = dx * (i + x_offset);
3173 if((shift >= 0) && (shift <= width)){
3175 ctx.translate(x1 + shift, y1 + label + voffset);
3176 ctx.rotate(-angle / 180 * Math.PI);
3177 ctx.fillText(category[i], 0, 0);
3183 return maxHeight + 2;
3186 /** Download the figure as an image or data
3187 * @param {string} mode either "CSV" | "PNG"
3191 let filename = this.param.title.text + "-" +
3193 ("0" + (d.getUTCMonth() + 1)).slice(-2) +
3194 ("0" + d.getUTCDate()).slice(-2) + "-" +
3195 ("0" + d.getUTCHours()).slice(-2) +
3196 ("0" + d.getUTCMinutes()).slice(-2) +
3197 ("0" + d.getUTCSeconds()).slice(-2);
3199 // use trick from FileSaver.js
3200 let a = document.getElementById('downloadHook');
3202 a = document.createElement("a");
3203 a.style.display = "none";
3204 a.id = "downloadHook";
3205 document.body.appendChild(a);
3208 if (mode === "CSV") {
3214 this.param.plot.forEach(g => {
3215 if (g.type === "scatter" || g.type === "bar") {
3224 for (let i = 0; i < g.xData.length; i++) {
3225 data += g.xData[i] + ",";
3226 data += g.yData[i] + "\n";
3231 if (g.type === "colormap") {
3235 for (let i = 0; i < g.nx; i++)
3236 data += g.xData[i] + ",";
3239 for (let j = 0; j < g.ny; j++) {
3240 data += g.yData[j] + ",";
3241 for (let i = 0; i < g.nx; i++)
3242 data += g.zData[i + j * g.nx] + ",";
3248 let blob = new Blob([data], {type: "text/csv"});
3249 let url = window.URL.createObjectURL(blob);
3252 a.download = filename;
3254 window.URL.revokeObjectURL(url);
3255 dlgAlert("Data downloaded to '" + filename + "'");
3257 } else if (mode === "PNG") {
3260 let smb = this.param.showMenuButtons;
3261 this.param.showMenuButtons = false;
3266 this.canvas.toBlob(function (blob) {
3267 let url = window.URL.createObjectURL(blob);
3270 a.download = filename;
3272 window.URL.revokeObjectURL(url);
3273 dlgAlert("Image downloaded to '" + filename + "'");
3275 h.param.showMenuButtons = smb;
3280 dlgAlert("Image download failed: " + e);
3281 this.param.showMenuButtons = smb;
3288 /** Draw a box encapsulating some text. Width and height are set by the text
3289 * @param {CanvasRenderingContext2D} ctx canvas context, for example: canvas.getContext("2d")
3290 * @param {string} text text to include in the box
3291 * @param {number} x coordinate of box lower left corner
3292 * @param {number} y coordinate of box lower left corner
3294 drawTextBox(ctx, text, x, y) {
3295 let line = text.split("\n");
3298 for (const g of line)
3299 if (ctx.measureText(g).width > mw)
3300 mw = ctx.measureText(g).width;
3302 let h = parseInt(ctx.font) * 1.5;
3304 let c = ctx.fillStyle;
3305 ctx.fillStyle = "white";
3306 ctx.fillRect(x, y, w, h * line.length);
3308 ctx.strokeRect(x, y, w, h * line.length);
3310 for (let i=0 ; i<line.length ; i++)
3311 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
3314 /** Handle mouse events
3315 * @param {Object} e mouse event object, specifies type, buttons
3318 // execute callback if registered
3319 if (this.param.event) {
3321 if (this.param.plot[0].type === "colormap") {
3322 // pass plot column/row to callback
3323 let x = this.screenToX(e.offsetX);
3324 let y = this.screenToY(e.offsetY);
3325 let xMin = this.param.plot[0].xMin;
3326 let xMax = this.param.plot[0].xMax;
3327 let yMin = this.param.plot[0].yMin;
3328 let yMax = this.param.plot[0].yMax;
3329 let dx = (xMax - xMin) / this.nx;
3330 let dy = (yMax - yMin) / this.ny;
3331 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
3332 x > xMin && x < xMax && y > yMin && y < yMax) {
3333 let ix = Math.floor((x - xMin) / dx);
3334 let iy = Math.floor((y - yMin) / dy);
3336 let flag = eval(this.param.event + "(e, this, ix, iy)");
3342 // call all other plots only with event and object
3343 let flag = eval(this.param.event + "(e, this)");
3350 // fix buttons for IE
3351 if (!e.which && e.button) {
3352 if ((e.button & 1) > 0) e.which = 1; // Left
3353 else if ((e.button & 4) > 0) e.which = 2; // Middle
3354 else if ((e.button & 2) > 0) e.which = 3; // Right
3361 // cancel dragging in case we did not catch the mouseup event
3362 if (e.type === "mousemove" && e.buttons === 0 &&
3363 (this.drag.active || this.zoom.x.active || this.zoom.y.active))
3366 if (e.type === "mousedown") {
3368 this.downloadSelector.style.display = "none";
3370 // check for buttons
3371 this.button.forEach(b => {
3372 if (e.offsetX > b.x1 && e.offsetX < b.x1 + b.width &&
3373 e.offsetY > b.y1 && e.offsetY < b.y1 + b.width &&
3379 // check for dragging
3380 if (e.offsetX > this.x1 && e.offsetX < this.x2 &&
3381 e.offsetY > this.y2 && e.offsetY < this.y1) {
3382 this.drag.active = true;
3383 this.marker.active = false;
3384 this.drag.sxStart = e.offsetX;
3385 this.drag.syStart = e.offsetY;
3386 this.drag.xStart = this.screenToX(e.offsetX);
3387 this.drag.yStart = this.screenToY(e.offsetY);
3388 this.drag.xMinStart = this.xMin;
3389 this.drag.xMaxStart = this.xMax;
3390 this.drag.yMinStart = this.yMin;
3391 this.drag.yMaxStart = this.yMax;
3393 this.blockAutoScale = true;
3396 // check for axis dragging
3397 if (e.offsetX > this.x1 && e.offsetX < this.x2 && e.offsetY > this.y1) {
3398 this.zoom.x.active = true;
3399 this.zoom.x.x1 = e.offsetX;
3400 this.zoom.x.x2 = undefined;
3401 this.zoom.x.t1 = this.screenToX(e.offsetX);
3403 if (e.offsetY < this.y1 && e.offsetY > this.y2 && e.offsetX < this.x1) {
3404 this.zoom.y.active = true;
3405 this.zoom.y.y1 = e.offsetY;
3406 this.zoom.y.y2 = undefined;
3407 this.zoom.y.v1 = this.screenToY(e.offsetY);
3410 } else if (cancel || e.type === "mouseup") {
3412 if (this.drag.active)
3413 this.drag.active = false;
3415 if (this.zoom.x.active) {
3416 if (this.zoom.x.x2 !== undefined &&
3417 Math.abs(this.zoom.x.x1 - this.zoom.x.x2) > 5) {
3418 let x1 = this.zoom.x.t1;
3419 let x2 = this.screenToX(this.zoom.x.x2);
3421 [x1, x2] = [x2, x1];
3425 this.zoom.x.active = false;
3426 this.blockAutoScale = true;
3430 if (this.zoom.y.active) {
3431 if (this.zoom.y.y2 !== undefined &&
3432 Math.abs(this.zoom.y.y1 - this.zoom.y.y2) > 5) {
3433 let y1 = this.zoom.y.v1;
3434 let y2 = this.screenToY(this.zoom.y.y2);
3436 [y1, y2] = [y2, y1];
3440 this.zoom.y.active = false;
3441 this.blockAutoScale = true;
3445 } else if (e.type === "mousemove") {
3446 if (this.drag.active) {
3451 if (this.param.xAxis.log) {
3452 let dx = e.offsetX - this.drag.sxStart;
3454 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));
3455 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));
3462 let dx = (e.offsetX - this.drag.sxStart) / (this.x2 - this.x1) * (this.xMax - this.xMin);
3463 this.xMin = this.drag.xMinStart - dx;
3464 this.xMax = this.drag.xMaxStart - dx;
3467 if (this.param.yAxis.log) {
3468 let dy = e.offsetY - this.drag.syStart;
3470 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));
3471 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));
3478 let dy = (this.drag.syStart - e.offsetY) / (this.y1 - this.y2) * (this.yMax - this.yMin);
3479 this.yMin = this.drag.yMinStart - dy;
3480 this.yMax = this.drag.yMaxStart - dy;
3487 // change cursor to pointer over buttons
3488 this.button.forEach(b => {
3489 if (e.offsetX > b.x1 && e.offsetY > b.y1 &&
3490 e.offsetX < b.x1 + b.width && e.offsetY < b.y1 + b.height) {
3496 // execute axis zoom
3497 if (this.zoom.x.active) {
3498 this.zoom.x.x2 = Math.max(this.x1, Math.min(this.x2, e.offsetX));
3499 this.zoom.x.t2 = this.screenToX(e.offsetX);
3502 if (this.zoom.y.active) {
3503 this.zoom.y.y2 = Math.max(this.y2, Math.min(this.y1, e.offsetY));
3504 this.zoom.y.v2 = this.screenToY(e.offsetY);
3508 // check if cursor close to plot point
3509 if (this.param.plot.length > 0) {
3510 if (this.param.plot[0].type === "scatter" || this.param.plot[0].type === "histogram" ||
3511 this.param.plot[0].type === "bar") {
3512 let minDist = 10000;
3513 for (const [pi, p] of this.param.plot.entries()) {
3514 if (p.xData === undefined || p.xData === null)
3517 for (let i = 0; i < p.xData.length; i++) {
3519 if (p.type === "bar")
3520 x = this.xToScreen(i + 0.5);
3522 x = this.xToScreen(p.xData[i]);
3523 let y = this.yToScreen(p.yData[i]);
3524 let d = (e.offsetX - x) * (e.offsetX - x) +
3525 (e.offsetY - y) * (e.offsetY - y);
3528 this.marker.x = p.xData[i];
3529 this.marker.y = p.yData[i];
3532 this.marker.mx = e.offsetX;
3533 this.marker.my = e.offsetY;
3534 this.marker.graphIndex = pi;
3535 this.marker.index = i;
3540 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
3543 if (this.param.plot[0].type === "colormap") {
3544 let x = this.screenToX(e.offsetX);
3545 let y = this.screenToY(e.offsetY);
3546 let xMin = this.param.plot[0].xMin;
3547 let xMax = this.param.plot[0].xMax;
3548 let yMin = this.param.plot[0].yMin;
3549 let yMax = this.param.plot[0].yMax;
3550 let dx = (xMax - xMin) / this.nx;
3551 let dy = (yMax - yMin) / this.ny;
3552 if (x > this.xMin && x < this.xMax && y > this.yMin && y < this.yMax &&
3553 x > xMin && x < xMax && y > yMin && y < yMax) {
3554 let i = Math.floor((x - xMin) / dx);
3555 let j = Math.floor((y - yMin) / dy);
3557 this.marker.x = (i + 0.5) * dx + xMin;
3558 this.marker.y = (j + 0.5) * dy + yMin;
3559 this.marker.z = this.param.plot[0].zData[i + j * this.nx];
3561 this.marker.sx = this.xToScreen(this.marker.x);
3562 this.marker.sy = this.yToScreen(this.marker.y);
3563 this.marker.mx = e.offsetX;
3564 this.marker.my = e.offsetY;
3565 this.marker.graphIndex = 0;
3566 this.marker.active = true;
3568 this.marker.active = false;
3576 } else if (e.type === "wheel" && this.param.mouseWheelZoom) {
3578 // ignore if outside axis window
3579 if (e.offsetX < this.x1 || e.offsetX > this.x2 ||
3580 e.offsetY < this.y2 || e.offsetY > this.y1)
3583 let x = this.screenToX(e.offsetX);
3584 let y = this.screenToY(e.offsetY);
3585 // Guard against scale <= -1 otherwise this.xMin becomes larger than this.xMax
3586 let scale = Math.max(e.deltaY * 0.01, -0.9);
3588 let xMinOld = this.xMin;
3589 let xMaxOld = this.xMax;
3590 let yMinOld = this.yMin;
3591 let yMaxOld = this.yMax;
3593 if (this.param.xAxis.log) {
3596 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
3598 this.xMax *= 1 + scale * (1 - f);
3599 this.xMin /= 1 + scale * f;
3601 if (this.xMax <= this.xMin) {
3602 this.xMin = xMinOld;
3603 this.xMax = xMaxOld;
3607 let dx = (this.xMax - this.xMin) * scale;
3608 let f = (x - this.xMin) / (this.xMax - this.xMin);
3609 this.xMin = this.xMin - dx * f;
3610 this.xMax = this.xMax + dx * (1 - f);
3613 // avoid too high zoom (would kill axis rendering)
3614 if (this.xMax - this.xMin < 1E-10*(this.xMax0 - this.xMin0)) {
3615 this.xMin = xMinOld;
3616 this.xMax = xMaxOld;
3619 if (this.param.yAxis.log) {
3622 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
3623 let yMinOld = this.yMin;
3624 let yMaxOld = this.yMax;
3626 this.yMax *= 1 + scale * f;
3627 this.yMin /= 1 + scale * (1 - f);
3629 if (this.yMax <= this.yMin) {
3630 this.yMin = yMinOld;
3631 this.yMax = yMaxOld;
3635 let dy = (this.yMax - this.yMin) * scale;
3636 let f = (y - this.yMin) / (this.yMax - this.yMin);
3637 this.yMin = this.yMin - dy * f;
3638 this.yMax = this.yMax + dy * (1 - f);
3641 // avoid too high zoom (would kill axis rendering)
3642 if (this.yMax - this.yMin < 1E-10*(this.yMax0 - this.yMin0)) {
3643 this.yMin = yMinOld;
3644 this.yMax = yMaxOld;
3647 this.blockAutoScale = true;
3653 this.parentDiv.title = title;
3654 this.parentDiv.style.cursor = cursor;
3657 /** Reset min/max of x and y axes, redraws */
3659 this.xMin = this.xMin0;
3660 this.xMax = this.xMax0;
3661 this.yMin = this.yMin0;
3662 this.yMax = this.yMax0;
3663 this.blockAutoScale = false;