MIDAS
Loading...
Searching...
No Matches
mplot.js
Go to the documentation of this file.
1//
2// Name: mplot.js
3// Created by: Stefan Ritt
4//
5// Contents: JavaScript graph plotting routines
6//
7// Note: please load midas.js, mhttpd.js and control.js before mplot.js
8//
9
10/**
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.
14 */
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"
20];
21
22// Use Viridis color map from matplotlib for 2D plots
23let colorPalette = [
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)'
280];
281
282let defaultGraphParam = {
283
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
287
288 floating: false, // true if in a floating dialog box
289
290 // general colors
291 color: {
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
296 },
297
298 title: {
299 color: "#000000",
300 backgroundColor: "#808080",
301 textSize: 16, // font size
302 text: "" // text content for the title
303 },
304
305 legend: {
306 show: true,
307 color: "#D0D0D0",
308 backgroundColor: "#FFFFFF",
309 textColor: "#404040",
310 textSize: 12
311 },
312
313 barWidth: 0.3, // width of the bars in a bar plot [0, 1], where 1 fills the entire space between points
314
315 stats: { // the following are set by calcStats
316 show: true,
317 names: [], // names of the various stats, varies by plot type
318 values: []
319 },
320
321 xAxis: {
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
328 title: {
329 text: "", // axis label text
330 textSize: 16 // font size of the axis label text
331 }
332 },
333
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
340 title: {
341 text: "", // axis label text
342 textSize: 16 // font size of the axis label text
343 }
344 },
345
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
351 title: {
352 text: "", // axis label text
353 textSize: 16 // font size of the axis label text
354 }
355 },
356
357 plot: [] // this array holds the parameters for each plot (dataset). See defaultPlotParam
358};
359
360/**
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.
364 */
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.
368
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
374
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
378
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
386
387 xMin: undefined, // drawing limits
388 xMax: undefined, // drawing limits
389 yMin: undefined, // drawing limits
390 yMax: undefined, // drawing limits
391
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"
397 size: 10,
398 lineWidth: 2 // marker border width
399 },
400
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
407 }
408};
409
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
413 */
414function mplot_init() {
415
416 // go through all data-name="mplot" tags
417 let mPlot = document.getElementsByClassName("mplot");
418
419 for (let i = 0; i < mPlot.length; i++)
420 mPlot[i].mpg = new MPlotGraph(mPlot[i]);
421
422 loadMPlotData();
423
424 window.addEventListener('resize', windowResize);
425}
426
427function profile(flag) {
428 if (flag === true || flag === undefined) {
429 console.log("");
430 profile.startTime = new Date().getTime();
431 return;
432 }
433
434 let now = new Date().getTime();
435 console.log("Profile: " + flag + ": " + (now-profile.startTime) + "ms");
436 profile.startTime = new Date().getTime();
437}
438
439/** Resize all mplot objects as defined by their class */
440function windowResize() {
441 let mPlot = document.getElementsByClassName("mplot");
442 for (const m of mPlot)
443 m.mpg.resize();
444}
445
446function isObject(item) {
447 return (item && typeof item === 'object' && !Array.isArray(item));
448}
449
450String.prototype.stripZeros = function () {
451 let s = this.trim();
452 if (s.search("[.]") >= 0) {
453 let i = s.search("[e]");
454 if (i >= 0) {
455 while (s.charAt(i - 1) === "0") {
456 s = s.substring(0, i - 1) + s.substring(i);
457 i--;
458 }
459 if (s.charAt(i - 1) === ".")
460 s = s.substring(0, i - 1) + s.substring(i);
461 } else {
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);
466 }
467 }
468 return s;
469};
470
471CanvasRenderingContext2D.prototype.drawLine = function (x1, y1, x2, y2) {
472 this.beginPath();
473 this.moveTo(x1, y1);
474 this.lineTo(x2, y2);
475 this.stroke();
476};
477
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
482 */
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]);
489 } else {
490 Object.assign(target, { [key]: source[key] });
491 }
492 }
493 }
494 return target;
495}
496
497/** Load data from the ODB for all HTML elements with the mplot class name */
498function loadMPlotData() {
499
500 // go through all data-name="mplot" tags
501 let mPlot = document.getElementsByClassName("mplot");
502
503 let v = [];
504 for (const mp of mPlot) {
505 for (const pl of mp.mpg.param.plot) {
506 if (pl.odbPath === undefined || pl.odbPath === "")
507 continue;
508
509 let name = pl.label;
510 if (name === "")
511 name = mp.id;
512
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+ "\"";
516 mp.mpg.draw();
517 pl.invalid = true;
518 continue;
519 }
520
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 + "\"";
524 mp.mpg.draw();
525 pl.invalid = true;
526 continue;
527 }
528
529 if (pl.odbPath.slice(-1) !== '/')
530 pl.odbPath += '/';
531
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);
542 }
543 }
544
545 mjsonrpc_db_get_values(v).then( function(rpc) {
546
547 let mPlot = document.getElementsByClassName("mplot");
548 let i = 0;
549 let iGraph = 0;
550 for (let mp of mPlot) {
551 for (let plt of mp.mpg.param.plot) {
552 if (!plt.odbPath === undefined || plt.odbPath === "" || plt.invalid)
553 continue;
554
555 let name = plt.label;
556 if (name === "")
557 name = mp.id;
558
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")) {
565
566 // switch plot to category plot if sting array found for x-data
567 plt.type = "bar";
568 plt.marker = undefined;
569 mp.mpg.param.xAxis.type = "category";
570 }
571 }
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 + "\"";
576 }
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 + "\"";
581 }
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 + "\"";
586 }
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 + "\"";
591 }
592
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
599 plt.xMin = 0;
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
604 plt.xMin = -0.5;
605 plt.xMax = plt.yData.length - 0.5;
606
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") {
611 plt.xMin = 0;
612 plt.xMax = plt.xData.length;
613 }
614 } else {
615 plt.xMin = Math.min(...plt.xData);
616 plt.xMax = Math.max(...plt.xData);
617 }
618
619 plt.yMin = Math.min(...plt.yData);
620 plt.yMax = Math.max(...plt.yData);
621
622 if (plt.type === "bar")
623 plt.yMin = 0;
624 }
625
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)));
629
630 if (plt.xMin === undefined) {
631 plt.xMin = -0.5;
632 plt.xMax = plt.nx - 0.5;
633 }
634 if (plt.yMin === undefined) {
635 plt.yMin = -0.5;
636 plt.yMax = plt.ny - 0.5;
637 }
638
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);
642
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);
646 }
647
648 iGraph++;
649 }
650 }
651
652 for (const mp of mPlot) {
653 if (!mp.mpg.blockAutoScale)
654 mp.mpg.calcMinMax();
655 mp.mpg.redraw();
656 }
657
658 // refresh data once per second
659 window.setTimeout(loadMPlotData, 1000);
660
661 }).catch( (error) => {
662 dlgAlert(error)
663 });
664}
665
666LN10 = 2.302585094;
667LOG2 = 0.301029996;
668LOG5 = 0.698970005;
669
670/* Begin ptimeToLabel format options */
671let poptions1 = {
672 timeZone: 'UTC',
673 day: '2-digit', month: 'short', year: '2-digit',
674 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
675};
676
677let poptions2 = {
678 timeZone: 'UTC',
679 day: '2-digit', month: 'short', year: '2-digit',
680 hour12: false, hour: '2-digit', minute: '2-digit'
681};
682
683let poptions3 = {
684 timeZone: 'UTC',
685 day: '2-digit', month: 'short', year: '2-digit',
686 hour12: false, hour: '2-digit', minute: '2-digit'
687};
688
689let poptions4 = {
690 timeZone: 'UTC',
691 day: '2-digit', month: 'short', year: '2-digit'
692};
693
694let poptions5 = {
695 timeZone: 'UTC',
696 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
697};
698
699let poptions6 = {
700 timeZone: 'UTC',
701 hour12: false, hour: '2-digit', minute: '2-digit'
702};
703
704let poptions7 = {
705 timeZone: 'UTC',
706 hour12: false, hour: '2-digit', minute: '2-digit'
707};
708
709let poptions8 = {
710 timeZone: 'UTC',
711 day: '2-digit', month: 'short', year: '2-digit',
712 hour12: false, hour: '2-digit', minute: '2-digit'
713};
714
715let poptions9 = {
716 timeZone: 'UTC',
717 day: '2-digit', month: 'short', year: '2-digit'
718};
719/* End ptimeToLabel format options */
720
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
726 */
727function ptimeToLabel(sec, base, forceDate) {
728 let d = mhttpd_get_display_time(sec).date;
729
730 if (forceDate) {
731 if (base < 60) {
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);
737 } else {
738 return d.toLocaleDateString('en-GB', poptions4);
739 }
740 }
741
742 if (base < 60) {
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);
750 } else {
751 return d.toLocaleDateString('en-GB', poptions9);
752 }
753}
754
755function checkPlotType(type) {
756 return type === "histogram" || type === "bar" || type === "scatter" ||
757 type === "colormap";
758}
759
760function mpgDialog(p) {
761 let width = window.innerWidth-200;
762 let height = window.innerHeight-200;
763
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;
774
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);
781 dlgShow(dlgFrame);
782
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);
794
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();
803 dlgPanel.mpg.draw();
804
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";
809
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();
814 }, 10);
815 }).observe(dlgFrame);
816}
817
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.
821 */
822class MPlotGraph{
823
824 /**
825 * @constructor
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.
828 * @example
829 * let graph = MPlotGraph();
830 * graph.addPlot({xData:[1,2,3], yData:[4,5,6]});
831 * graph.draw();
832 */
833 constructor(divElement, graphParam) {
834
835 if (divElement === undefined || divElement === null) {
836 dlgAlert("MPlot constructor called without valid divElement");
837 return;
838 }
839
840 // save parameters from <div>
841 this.parentDiv = divElement;
842 this.divParam = divElement.innerHTML;
843 divElement.innerHTML = "";
844
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);
848
849 // default parameters
850 this.param = JSON.parse(JSON.stringify(defaultGraphParam)); // deep copy
851
852 // overwrite default parameters from <div> text body
853 try {
854 if (this.divParam.includes('{')) {
855 let p = JSON.parse(this.divParam);
856 this.param = deepMerge(this.param, p);
857 }
858 } catch (error) {
859 this.parentDiv.innerHTML = "<pre>" + this.divParam + "</pre>";
860 dlgAlert(error);
861 return;
862 }
863
864 // obtain parameters form <div> attributes ---
865
866 // data-odb-path
867 if (this.parentDiv.dataset.odbPath)
868 this.param.odbPath = this.parentDiv.dataset.odbPath;
869
870 // data-type
871 if (this.parentDiv.dataset.type)
872 this.param.type = this.parentDiv.dataset.type;
873
874 // data-title
875 if (this.parentDiv.dataset.title)
876 this.param.title.text = this.parentDiv.dataset.title;
877
878 // data-x/y/z-text
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;
885
886 // data-x/y
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";
892 }
893
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;
898
899 // data-x/y-error
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;
904
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);
918
919 // data-x/y/z-log
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;
931 }
932 }
933
934 // data-h
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;
946 }
947 }
948
949 // data-z
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>");
968 return;
969 }
970 if (this.param.plot[0].ny === undefined) {
971 dlgAlert("\"data-ny\" missing for colormap mplot <div>");
972 return;
973 }
974 }
975
976 // data-label
977 if (this.parentDiv.dataset.label)
978 this.param.plot[0].label = this.parentDiv.dataset.label;
979
980 // data-line-width
981 if (this.parentDiv.dataset["line-width"])
982 this.param.plot[0].line.width = this.parentDiv.dataset["line-width"];
983
984 // data-bar-width
985 if (this.parentDiv.dataset["bar-width"])
986 this.param.barWidth = this.parentDiv.dataset["bar-width"];
987
988 // data-marker
989 if (this.parentDiv.dataset["marker-style"])
990 this.param.plot[0].marker.style = this.parentDiv.dataset["marker-style"];
991
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];
1001
1002 if (this.parentDiv.dataset["x" + i])
1003 this.param.plot[index].xPath = this.parentDiv.dataset["x" + i];
1004 else
1005 // copy from first plot if we only have "data-x"
1006 this.param.plot[index].xPath = this.param.plot[0].xPath;
1007
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"];
1024 }
1025 }
1026
1027 // data-h<n>
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];
1035
1036 this.param.plot[index].type = "histogram";
1037 this.param.plot[index].yPath = this.parentDiv.dataset["h"+i];
1038
1039 this.param.plot[index].xMin = this.param.xAxis.min;
1040 this.param.plot[index].xMax = this.param.xAxis.max;
1041
1042 if (this.parentDiv.dataset["label"+i])
1043 this.param.plot[index].label = this.parentDiv.dataset["label"+i];
1044 }
1045 }
1046
1047 // data-xaxis-type
1048 if (this.parentDiv.dataset["x-type"])
1049 this.param.xAxis.type = this.parentDiv.dataset["x-type"];
1050
1051 // data-overlay
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('('));
1056 }
1057
1058 // data-event
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('('));
1063 }
1064
1065 // data-stats
1066 if (this.parentDiv.dataset.stats)
1067 this.param.stats.show = (this.parentDiv.dataset.stats === "1");
1068
1069 // set parameters from constructor
1070 if (graphParam)
1071 deepMerge(this.param, graphParam);
1072
1073 // check plot type
1074 for (const p of this.param.plot) {
1075 if (!checkPlotType(p.type))
1076 throw new Error(`mplot.js: Unknown plot type "${p.type}"`);
1077 }
1078
1079 // dragging
1080 this.drag = {
1081 active: false,
1082 sxStart: 0,
1083 syStart: 0,
1084 xStart: 0,
1085 yStart: 0,
1086 xMinStart: 0,
1087 xMaxStart: 0,
1088 yMinStart: 0,
1089 yMaxStart: 0,
1090 };
1091
1092 // axis zoom
1093 this.zoom = {
1094 x: {active: false},
1095 y: {active: false}
1096 };
1097
1098 // marker
1099 this.marker = {active: false};
1100 this.blockAutoScale = false;
1101
1102 this.error = null;
1103
1104 // buttons
1105 this.button = [
1106 {
1107 src: "maximize.svg",
1108 title: "Maximize this plot",
1109 click: function (t) {
1110 mpgDialog(t);
1111 }
1112 },
1113 {
1114 src: "menu.svg",
1115 title: "Show / hide legend",
1116 click: function (t) {
1117 t.param.legend.show = !t.param.legend.show;
1118
1119 let nLabel = 0;
1120 for (const g of t.param.plot)
1121 if (g.label && g.label !== "")
1122 nLabel++;
1123 if (nLabel === 0)
1124 dlgAlert("No plot labels defined");
1125
1126 t.redraw();
1127 }
1128 },
1129 {
1130 src: "stats.svg",
1131 title: "Show / hide statistics",
1132 click: function (t) {
1133 t.param.stats.show = !t.param.stats.show;
1134 t.redraw();
1135 }
1136 },
1137 {
1138 src: "rotate-ccw.svg",
1139 title: "Reset histogram axes",
1140 click: function (t) {
1141 t.resetAxes();
1142 }
1143 },
1144 {
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 +
1154 this.y1) + "px";
1155 t.downloadSelector.style.zIndex = "32";
1156 } else {
1157 t.downloadSelector.style.display = "none";
1158 }
1159 }
1160 },
1161 ];
1162
1163 // remove maximize button if we are floating
1164 if (this.param.floating)
1165 this.button.shift();
1166
1167 this.button.forEach(b => {
1168 b.img = new Image();
1169 b.img.src = "icons/" + b.src;
1170 });
1171
1172 this.createDownloadSelector();
1173
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);
1180
1181 // Keyboard event handler (has to be on the window!)
1182 window.addEventListener("keydown", this.keyDown.bind(this));
1183
1184 // create canvas
1185 this.canvas = document.createElement("canvas");
1186 this.canvas.style.border = "1px solid black";
1187
1188 if (parseInt(this.parentDiv.style.width) > 0)
1189 this.canvas.width = parseInt(this.parentDiv.style.width);
1190 else
1191 this.canvas.width = 500;
1192 if (parseInt(this.parentDiv.style.height) > 0)
1193 this.canvas.height = parseInt(this.parentDiv.style.height);
1194 else
1195 this.canvas.height = 300;
1196
1197 divElement.appendChild(this.canvas);
1198 }
1199
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]});
1206 */
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];
1211 g.type = "scatter";
1212 g.marker.lineColor = defaultGraphColor[index];
1213 g.marker.fillColor = defaultGraphColor[index];
1214 g.line.color = defaultGraphColor[index];
1215
1216 // merge optional paramters
1217 if (plotParam) {
1218 deepMerge(g, plotParam);
1219
1220 if (plotParam.xData || plotParam.yData || plotParam.zData)
1221 this.setData(index, plotParam.xData, plotParam.yData, plotParam.zData);
1222 }
1223
1224 return index;
1225 }
1226
1227 findPlot(label) {
1228 let index = -1;
1229 if (typeof label === "string") {
1230 for (const [i,p] of this.param.plot.entries()) {
1231 if (p.label === label) {
1232 index = i;
1233 break;
1234 }
1235 }
1236 if (index === -1) {
1237 alert("Plot \"" + label + "\" not found");
1238 return -1;
1239 }
1240 } else
1241 index = label;
1242
1243 if (index < 0 || index > this.param.plot.length) {
1244 alert("Invalid index \"" + index + "\"");
1245 return -1;
1246 }
1247
1248 return index;
1249 }
1250
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"}} );
1257 */
1258 modifyPlot(label, plotParam) {
1259 let index = this.findPlot(label);
1260 deepMerge(this.param.plot[index], plotParam);
1261 }
1262
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);
1266 */
1267 deletePlot(label) {
1268 let index = this.findPlot(label);
1269 this.param.plot.splice(index, 1);
1270 }
1271
1272 /** Make a download selection menu */
1273 createDownloadSelector () {
1274
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";
1287
1288 this.downloadSelector.style.left = "100px";
1289 this.downloadSelector.style.top = "100px";
1290
1291 let table = document.createElement("table");
1292 let mhg = this;
1293
1294 let row = document.createElement("tr");
1295 let cell = document.createElement("td");
1296 cell.style.padding = "0";
1297 let link = document.createElement("a");
1298 link.href = "#";
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");
1304 return false;
1305 }.bind(this);
1306 cell.appendChild(link);
1307 row.appendChild(cell);
1308 table.appendChild(row);
1309
1310 row = document.createElement("tr");
1311 cell = document.createElement("td");
1312 cell.style.padding = "0";
1313 link = document.createElement("a");
1314 link.href = "#";
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");
1320 return false;
1321 }.bind(this);
1322 cell.appendChild(link);
1323 row.appendChild(cell);
1324 table.appendChild(row);
1325
1326 this.downloadSelector.appendChild(table);
1327 document.body.appendChild(this.downloadSelector);
1328 }
1329
1330 /** Handle key events
1331 * @param {Object} e keydown event with properties key, metaKey, ctrlKey, target, etc
1332 */
1333 keyDown(e) {
1334 if (e.key === "r" && !e.ctrlKey && !e.metaKey) { // 'r' key
1335
1336 // don't grab key if we are in an input field
1337 if (e.target.tagName === "INPUT")
1338 return;
1339
1340 this.resetAxes();
1341 e.preventDefault();
1342 }
1343 }
1344
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
1351 */
1352 setData(index, x, y, z) {
1353
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 + "\"");
1357 return;
1358 }
1359
1360 let g = this.param.plot[index];
1361
1362 g.odbPath = ""; // prevent loading of ODB data
1363
1364 // check plot type
1365 if (!checkPlotType(g.type))
1366 throw new Error(`mplot.js: Unknown plot type "${g.type}"`);
1367
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) {
1373 g.xMin = -0.5;
1374 g.xMax = g.yData.length - 0.5;
1375 }
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);
1380
1381 g.yData = y;
1382
1383 g.yMin = Math.min(...g.yData);
1384 g.yMax = Math.max(...g.yData);
1385 }
1386
1387 else if (g.type === "bar") {
1388 g.xData = x;
1389 g.yData = y;
1390
1391 g.xMin = 0;
1392 g.xMax = x.length;
1393 g.yMin = 0;
1394 g.yMax = Math.max(...g.yData);
1395 }
1396
1397 else if (g.type === "scatter" ) {
1398 g.xData = x;
1399 g.yData = y;
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);
1404 }
1405
1406 else if (g.type === "colormap") {
1407 g.zData = z;
1408 g.zMin = Math.min(...g.zData.filter(v=>!isNaN(v)));
1409 g.zMax = Math.max(...g.zData.filter(v=>!isNaN(v)));
1410
1411 if (g.xMin === undefined) {
1412 g.xMin = -0.5;
1413 g.xMax = g.nx - 0.5;
1414 }
1415 if (g.yMin === undefined) {
1416 g.yMin = -0.5;
1417 g.yMax = g.ny - 0.5;
1418 }
1419
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);
1423
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);
1427 }
1428
1429 if (!this.blockAutoScale) {
1430 this.calcMinMax();
1431 }
1432
1433 this.redraw();
1434 }
1435
1436 /** Resize canvas and redraw */
1437 resize() {
1438 this.canvas.width = this.parentDiv.clientWidth;
1439 this.canvas.height = this.parentDiv.clientHeight;
1440 this.redraw();
1441 }
1442
1443 /** Redraw the graph */
1444 redraw() {
1445 let f = this.draw.bind(this);
1446 window.requestAnimationFrame(f);
1447 }
1448
1449 /** Convert data coordinates to screen coordinates x axis */
1450 xToScreen(x) {
1451
1452 if (this.param.xAxis.log) {
1453 if (x <= 0)
1454 return this.x1;
1455 else
1456 return this.x1 + (Math.log(x) - Math.log(this.xMin)) /
1457 (Math.log(this.xMax) - Math.log(this.xMin)) * (this.x2 - this.x1);
1458 }
1459 return this.x1 + (x - this.xMin) / (this.xMax - this.xMin) * (this.x2 - this. x1);
1460 }
1461
1462 /** Convert data coordinates to screen coordinates y axis */
1463 yToScreen(y) {
1464 if (this.param.yAxis.log) {
1465 if (y <= 0)
1466 return this.y1;
1467 else
1468 return this.y1 - (Math.log(y) - Math.log(this.yMin)) /
1469 (Math.log(this.yMax) - Math.log(this.yMin)) * (this.y1 - this.y2);
1470 }
1471 return this.y1 - (y - this.yMin) / (this.yMax - this.yMin) * (this.y1 - this. y2);
1472 }
1473
1474 /** Convert screen coordinates to data coordinates x axis */
1475 screenToX(x) {
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);
1479 }
1480 return (x - this.x1) / (this.x2 - this.x1) * (this.xMax - this.xMin) + this.xMin;
1481 };
1482
1483 /** Convert screen coordinates to data coordinates y axis */
1484 screenToY(y) {
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);
1488 }
1489 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
1490 };
1491
1492 /** Calculate the min/max of each axis */
1493 calcMinMax() {
1494
1495 if (this.param.plot.length === 0)
1496 return;
1497
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;
1502
1503 if (this.param.zAxis.min !== undefined)
1504 this.zMin = this.param.zAxis.min;
1505 else
1506 this.zMin = this.param.plot[0].zMin;
1507
1508 if (this.param.zAxis.max !== undefined)
1509 this.zMax = this.param.zAxis.max;
1510 else
1511 this.zMax = this.param.plot[0].zMax;
1512
1513 if (this.param.zAxis.log) {
1514 if (this.zMin < 1E-20)
1515 this.zMin = 1E-20;
1516 if (this.zMax < 1E-18)
1517 this.zMax = 1E-18;
1518 }
1519
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;
1524
1525 this.xMin0 = this.xMin;
1526 this.xMax0 = this.xMax;
1527 this.yMin0 = this.yMin;
1528 this.yMax0 = this.yMax;
1529 return;
1530 }
1531
1532 // determine min/max of overall plot
1533 let xMin = this.param.plot[0].xMin;
1534 for (const g of this.param.plot)
1535 if (g.xMin < xMin)
1536 xMin = g.xMin;
1537 if (this.param.xAxis.min !== undefined)
1538 xMin = this.param.xAxis.min;
1539
1540 let xMax = this.param.plot[0].xMax;
1541 for (const g of this.param.plot)
1542 if (g.xMax > xMax)
1543 xMax = g.xMax;
1544 if (this.param.xAxis.max !== undefined)
1545 xMax = this.param.xAxis.max;
1546
1547 let yMin = this.param.plot[0].yMin;
1548 for (const g of this.param.plot)
1549 if (g.yMin < yMin)
1550 yMin = g.yMin;
1551 if (this.param.yAxis.min !== undefined)
1552 yMin = this.param.yAxis.min;
1553
1554 let yMax = this.param.plot[0].yMax;
1555 for (const g of this.param.plot)
1556 if (g.yMax > yMax)
1557 yMax = g.yMax;
1558 if (this.param.yAxis.max !== undefined)
1559 yMax = this.param.yAxis.max;
1560
1561 // avoid min === max
1562 if (xMin === xMax) { xMin -= 0.5; xMax += 0.5; }
1563 if (yMin === yMax) { yMin -= 0.5; yMax += 0.5; }
1564
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)
1570 xMin -= dx / 20;
1571 if (this.param.xAxis.max === undefined)
1572 xMax += dx / 20;
1573 if (this.param.yAxis.min === undefined)
1574 yMin -= dy / 20;
1575 }
1576 if (this.param.yAxis.max === undefined)
1577 yMax += dy / 20;
1578
1579 this.xMin = xMin;
1580 this.xMax = xMax;
1581 this.yMin = yMin;
1582 this.yMax = yMax;
1583
1584 this.xMin0 = xMin;
1585 this.xMax0 = xMax;
1586 this.yMin0 = yMin;
1587 this.yMax0 = yMax;
1588 }
1589
1590 /** Calculate the stats to display in the graph
1591 * These are stored in MPlotGraph.param.stats. See also defaultGraphParam
1592 */
1593 calcStats() {
1594 this.stats = {};
1595 let g = this.param.plot[0];
1596
1597 if (g.type === "scatter") {
1598 this.stats.name = ["Entries", "Mean X", "Std Dev X", "Mean Y", "Std Dev Y"];
1599
1600 this.stats.value = Array(5).fill(0);
1601 let n = this.param.plot[0].xData.length;
1602
1603 if (n > 1) {
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));
1610
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));
1616 }
1617 }
1618
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);
1623
1624 if (n > 1) {
1625 let sumY = 0;
1626 let sumXY = 0;
1627 let sumX2Y = 0;
1628
1629 for (let i=0 ; i< g.xData.length ; i++) {
1630 sumY += g.yData[i];
1631 sumXY += g.xData[i] * g.yData[i];
1632 sumX2Y += g.xData[i] * g.xData[i] * g.yData[i];
1633 }
1634
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));
1641 }
1642 }
1643
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;
1649
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);
1653 }
1654 }
1655
1656 if (g.type === "colormap") {
1657 this.stats.name = ["Entries", "Mean X", "Mean Y", "Std Dev X", "Std Dev Y"];
1658
1659 this.stats.value = Array(5).fill(0);
1660
1661 if (g.nx > 1 && g.ny > 1) {
1662 let n = 0;
1663
1664 // calculate x/y values
1665 let dx = (g.xMax - g.xMin) / this.nx;
1666 let dy = (g.yMax - g.yMin) / this.ny;
1667
1668 let xi = Array.from({ length: g.nx }, (_, i) =>
1669 g.xMin + (i + 0.5) * dx);
1670
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];
1677 }
1678 }
1679
1680 let sumY = 0;
1681 let sumXY = 0;
1682 let sumX2Y = 0;
1683
1684 for (let i=0 ; i< g.nx ; i++) {
1685 sumY += sumH[i];
1686 sumXY += xi[i] * sumH[i];
1687 sumX2Y += xi[i] * xi[i] * sumH[i];
1688 }
1689
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));
1696
1697 //----------------------------------------------
1698
1699 xi = Array.from({ length: g.ny }, (_, i) =>
1700 g.yMin + (i + 0.5) * dy);
1701
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];
1707 }
1708 }
1709
1710 sumY = 0;
1711 sumXY = 0;
1712 sumX2Y = 0;
1713
1714 for (let i=0 ; i< g.ny ; i++) {
1715 sumY += sumH[i];
1716 sumXY += xi[i] * sumH[i];
1717 sumX2Y += xi[i] * xi[i] * sumH[i];
1718 }
1719
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));
1725 }
1726 }
1727
1728 }
1729
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
1735 */
1736 drawMarker(ctx, p, x, y) {
1737 ctx.strokeStyle = p.marker.lineColor;
1738 ctx.fillStyle = p.marker.fillColor;
1739
1740 let size = p.marker.size;
1741 ctx.lineWidth = p.marker.lineWidth;
1742
1743 switch(p.marker.style) {
1744 case "circle":
1745 ctx.beginPath();
1746 ctx.arc(x, y, size / 2, 0, 2 * Math.PI);
1747 ctx.fill();
1748 ctx.stroke();
1749 break;
1750 case "square":
1751 ctx.fillRect(x - size / 2, y - size / 2, size, size);
1752 ctx.strokeRect(x - size / 2, y - size / 2, size, size);
1753 break;
1754 case "diamond":
1755 ctx.beginPath();
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);
1761 ctx.fill();
1762 ctx.stroke();
1763 break;
1764 case "pentagon":
1765 ctx.beginPath();
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);
1772 ctx.fill();
1773 ctx.stroke();
1774 break;
1775 case "triangle-up":
1776 ctx.beginPath();
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);
1781 ctx.fill();
1782 ctx.stroke();
1783 break;
1784 case "triangle-down":
1785 ctx.beginPath();
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);
1790 ctx.fill();
1791 ctx.stroke();
1792 break;
1793 case "triangle-left":
1794 ctx.beginPath();
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);
1799 ctx.fill();
1800 ctx.stroke();
1801 break;
1802 case "triangle-right":
1803 ctx.beginPath();
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);
1808 ctx.fill();
1809 ctx.stroke();
1810 break;
1811 case "cross":
1812 ctx.beginPath();
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);
1817 ctx.stroke();
1818 break;
1819 case "plus":
1820 ctx.beginPath();
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);
1825 ctx.stroke();
1826 break;
1827 }
1828 }
1829
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
1837 */
1838 drawXErrorBar(ctx, p, x, y, x1, x2) {
1839 let size = p.marker.size / 2;
1840
1841 ctx.beginPath();
1842 ctx.moveTo(x1, y);
1843 ctx.lineTo(x2, y);
1844 ctx.moveTo(x1, y-size);
1845 ctx.lineTo(x1, y+size);
1846 ctx.moveTo(x2, y-size);
1847 ctx.lineTo(x2, y+size);
1848 ctx.stroke();
1849 }
1850
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
1858 */
1859 drawYErrorBar(ctx, p, x, y, y1, y2) {
1860 let size = p.marker.size / 2;
1861
1862 ctx.beginPath();
1863 ctx.moveTo(x, y1);
1864 ctx.lineTo(x, y2);
1865 ctx.moveTo(x-size, y1);
1866 ctx.lineTo(x+size, y1);
1867 ctx.moveTo(x-size, y2);
1868 ctx.lineTo(x+size, y2);
1869 ctx.stroke();
1870 }
1871
1872 /** draw all elements of the graph into the canvas: axes, plots, text, buttons, etc. */
1873 draw() {
1874 //profile();
1875 if (!this.canvas || this.param.plot.length === 0)
1876 return;
1877
1878 let ctx = this.canvas.getContext("2d");
1879
1880 this.width = this.canvas.width;
1881 this.height = this.canvas.height;
1882
1883 ctx.fillStyle = this.param.color.background;
1884 ctx.fillRect(0, 0, this.width, this.height);
1885
1886 if (this.error !== null) {
1887 ctx.lineWidth = 1;
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);
1894 return;
1895 }
1896
1897 if (this.param.plot[0].xData === undefined) {
1898 ctx.lineWidth = 1;
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);
1905 return;
1906 }
1907
1908 if (this.height === undefined || this.width === undefined)
1909 return;
1910 if (this.param.plot[0].xMin === undefined || this.param.plot[0].xMax === undefined)
1911 return;
1912
1913 ctx.font = this.param.yAxis.textSize + "px sans-serif";
1914
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);
1917
1918 if (axisLabelWidth === undefined)
1919 return;
1920
1921 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "")
1922 this.x1 = axisLabelWidth + 5 + 2.5*this.param.yAxis.title.textSize;
1923 else
1924 this.x1 = axisLabelWidth + 15;
1925
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;
1929
1930 if (this.param.showMenuButtons === false)
1931 this.x2 = this.width - 2;
1932
1933 this.y1 = this.height;
1934 this.y2 = 6;
1935
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);
1940 else
1941 axisLabelHeight = this.param.xAxis.textSize;
1942
1943 axisLabelHeight += 12; // space for ticks and frame
1944
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;
1947 else
1948 this.y1 = this.height - axisLabelHeight;
1949
1950 if (this.param.plot[0].type === "colormap" && this.param.plot[0].showZScale) {
1951 if (this.zMin === undefined || this.zMax === undefined) {
1952 this.zMin = 0;
1953 this.zMax = 1;
1954 }
1955 if (this.zMin === this.zMax) {
1956 this.zMin -= 0.5;
1957 this.zMax += 0.5;
1958 }
1959
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)
1964 return;
1965
1966 if (this.param.zAxis.show) {
1967 let w = 5; // left gap
1968 w += 10; // color bar
1969 w += 12; // tick width
1970 w += 5;
1971
1972 this.x2 -= axisLabelWidth + w;
1973 this.param.zAxis.width = axisLabelWidth + w;
1974 }
1975 }
1976
1977 // title
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);
1989 this.y2 = 6 + h;
1990 }
1991
1992 // draw axis
1993 ctx.strokeStyle = this.param.color.axis;
1994
1995 if (this.param.yAxis.log && this.yMin < 1E-20)
1996 this.yMin = 1E-20;
1997 if (this.param.yAxis.log && this.yMax < 1E-18)
1998 this.yMax = 1E-18;
1999
2000 if (this.param.xAxis.title.text && this.param.xAxis.title.text !== "") {
2001 ctx.save();
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);
2009 ctx.restore();
2010 }
2011
2012 ctx.font = this.param.xAxis.textSize + "px sans-serif";
2013 let grid = this.param.xAxis.grid ? this.y2 - this.y1 : 0;
2014
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);
2024
2025 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
2026 ctx.save();
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);
2035 ctx.restore();
2036 }
2037
2038 if (this.param.zAxis.title.text && this.param.zAxis.title.text !== "") {
2039 ctx.save();
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);
2048 ctx.restore();
2049 }
2050
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);
2055
2056 if (this.param.yAxis.title.text && this.param.yAxis.title.text !== "") {
2057 ctx.save();
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);
2065 ctx.restore();
2066 }
2067
2068 // draw frame
2069 ctx.strokeStyle = this.param.color.axis;
2070 ctx.strokeRect(this.x1, this.y1, this.x2-this.x1, this.y2-this.y1);
2071
2072 // set clipping region not to draw outside axes
2073 ctx.save();
2074 ctx.rect(this.x1, this.y2, this.x2 - this.x1, this.y1 - this.y2);
2075 ctx.clip();
2076
2077 // draw graphs
2078 let noData = true;
2079 for (const [plotIndex,p] of this.param.plot.entries()) {
2080 if (p.xData === undefined || p.xData === null)
2081 continue;
2082
2083 if (p.xData.length > 0)
2084 noData = false;
2085
2086 ctx.globalAlpha = p.alpha;
2087
2088 if (p.type === "scatter") {
2089 // draw lines
2090 if (p.line && p.line.draw ||
2091 p.line && p.line.fill) {
2092
2093 ctx.fillStyle = p.line.color;
2094 ctx.strokeStyle = ctx.fillStyle;
2095
2096 // shaded area
2097 if (p.line.fill) {
2098 ctx.globalAlpha = 0.1;
2099 ctx.beginPath();
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]);
2104 ctx.lineTo(x, y);
2105 }
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));
2108 ctx.fill();
2109 ctx.globalAlpha = 1;
2110 }
2111
2112 // draw line
2113 if (p.line.draw && p.line.width > 0 && p.line.style !== "none") {
2114 ctx.lineWidth = p.line.width;
2115 ctx.beginPath();
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]);
2123 if (i === 0)
2124 ctx.moveTo(x, y);
2125 else
2126 ctx.lineTo(x, y);
2127 }
2128 ctx.stroke();
2129 ctx.setLineDash([]);
2130 }
2131 }
2132
2133 // draw markers
2134 if (p.marker && p.marker.draw) {
2135 for (let i = 0; i < p.xData.length; i++) {
2136
2137 let x = this.xToScreen(p.xData[i]);
2138 let y = this.yToScreen(p.yData[i]);
2139
2140 this.drawMarker(ctx, p, x, y);
2141
2142 if (p.xErrorData) {
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);
2146 }
2147
2148 if (p.yErrorData) {
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);
2152 }
2153 }
2154 }
2155 }
2156
2157 else if (p.type === "histogram") {
2158 let x, y;
2159 let dx = (p.xMax - p.xMin) / p.xData.length;
2160 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
2161
2162 if (p.length < 100)
2163 ctx.lineWidth = 2;
2164 else
2165 ctx.lineWidth = 1;
2166
2167 ctx.fillStyle = p.line.color;
2168 ctx.strokeStyle = ctx.fillStyle;
2169
2170 ctx.beginPath();
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);
2177 }
2178 ctx.lineTo(x+dxs/2, this.yToScreen(0));
2179 ctx.globalAlpha = 0.2;
2180 ctx.fill();
2181 ctx.globalAlpha = 1;
2182 ctx.stroke();
2183 }
2184
2185 else if (p.type === "bar") {
2186 let x, y;
2187 let dx = (this.x2 - this. x1) * (this.xMax0-this.xMin0) / (this.xMax-this.xMin) / p.xData.length;
2188
2189 let totalWidth;
2190 if (this.param.barWidth)
2191 totalWidth = dx * this.param.barWidth;
2192 else
2193 totalWidth = dx * 0.3;
2194
2195 let barWidth = totalWidth / this.param.plot.length; // each plot gets a fraction of the bar width
2196 let offset = plotIndex * barWidth;
2197
2198 if (p.xData.length < 100)
2199 ctx.lineWidth = 2;
2200 else
2201 ctx.lineWidth = 1;
2202
2203 ctx.fillStyle = p.line.color;
2204 ctx.strokeStyle = ctx.fillStyle;
2205
2206 ctx.beginPath();
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));
2215 }
2216 ctx.globalAlpha = 0.2;
2217 ctx.fill();
2218 ctx.globalAlpha = 1;
2219 ctx.stroke();
2220 }
2221
2222 else if (p.type === "colormap") {
2223 let dx = (p.xMax - p.xMin) / this.nx;
2224 let dy = (p.yMax - p.yMin) / this.ny;
2225
2226 let dxs = dx / (this.xMax - this.xMin) * (this.x2 - this. x1);
2227 let dys = dy / (this.yMax - this.yMin) * (this.y2 - this. y1);
2228
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];
2234 if (isNaN(zval)) {
2235 ctx.fillStyle = 'hsl(255, 0%, 50%)';
2236 } else {
2237 let v;
2238 if (this.param.zAxis.log) {
2239 if (zval <= 0)
2240 v = 0;
2241 else
2242 v = (Math.log(zval) - Math.log(this.zMin)) / (Math.log(this.zMax) - Math.log(this.zMin));
2243 } else
2244 v = (zval - this.zMin) / (this.zMax - this.zMin);
2245
2246 // limit v to 0...1
2247 if (v < 0)
2248 v = 0;
2249 if (v > 1)
2250 v = 1;
2251
2252 if (zval < 0.5 && this.param.plot[0].zeroColor)
2253 ctx.fillStyle = this.param.plot[0].zeroColor;
2254 else
2255 ctx.fillStyle = colorPalette[Math.floor((v)*255)];
2256 }
2257 ctx.fillRect(Math.floor(x), Math.floor(y), Math.floor(dxs+1), Math.floor(dys-1));
2258 }
2259 }
2260 //profile("plot");
2261 }
2262 }
2263
2264 ctx.restore(); // remove clipping
2265
2266 // plot color scale
2267 if (this.param.plot[0].type === "colormap") {
2268 if (this.param.plot[0].showZScale) {
2269
2270 for (let i=0 ; i<100 ; i++) {
2271 let v = i / 100;
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);
2275 }
2276
2277 ctx.lineWidth = 1;
2278 ctx.strokeStyle = this.param.color.axis;
2279 ctx.beginPath();
2280 ctx.rect(this.x2 + 5, this.y2, 10, this.y1 - this.y2);
2281 ctx.stroke();
2282
2283 ctx.font = this.param.zAxis.textSize + "px sans-serif";
2284 ctx.strokeStyle = this.param.color.axis;
2285
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);
2288 }
2289 }
2290
2291 // plot legend
2292 let nLabel = 0;
2293 for (const g of this.param.plot)
2294 if (g.label && g.label !== "")
2295 nLabel++;
2296
2297 if (this.param.legend?.show && nLabel > 0) {
2298 ctx.font = this.param.legend.textSize + "px sans-serif";
2299
2300 let mw = 0;
2301 for (const g of this.param.plot) {
2302 if (ctx.measureText(g.label).width > mw) {
2303 mw = ctx.measureText(g.label).width;
2304 }
2305 }
2306 let w = 50 + mw + 5;
2307 let h = this.param.legend.textSize * 1.5;
2308
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);
2313
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") {
2316 ctx.beginPath();
2317 ctx.strokeStyle = g.line.color;
2318 ctx.lineWidth = g.line.width;
2319
2320 if (g.line.style === "dashed")
2321 ctx.setLineDash([5,5]);
2322 if (g.line.style === "dotted")
2323 ctx.setLineDash([1,10]);
2324
2325 ctx.beginPath();
2326 ctx.moveTo(this.x1 + 5, this.y2 + gi*h + h/2);
2327 ctx.lineTo(this.x1 + 35, this.y2 + gi*h + h/2);
2328 ctx.stroke();
2329
2330 ctx.setLineDash([]);
2331 }
2332 ctx.strokeStyle = g.line.color;
2333 ctx.lineWidth = g.line.width;
2334 if (g.marker?.draw)
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);
2340 }
2341 }
2342
2343 this.calcStats();
2344
2345 // plot statistics
2346 if (this.param.stats.show && this.stats.name) {
2347 ctx.font = this.param.legend.textSize + "px sans-serif";
2348
2349 let mw = 0;
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;
2354 }
2355 }
2356 let w = mw + 10;
2357 let h = this.param.legend.textSize * 1.5;
2358
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);
2363
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);
2372 }
2373 }
2374
2375 // "empty window" notice
2376 if (noData) {
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;
2382 let h = 16 + 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";
2390 }
2391
2392 // buttons
2393 if (this.param.showMenuButtons) {
2394 let y = 0;
2395 let buttonSize = 20;
2396 this.button.forEach(b => {
2397
2398 b.x1 = this.width - buttonSize - 6;
2399 b.y1 = 6 + y * (buttonSize + 4);
2400 b.width = buttonSize + 4;
2401 b.height = buttonSize + 4;
2402 b.enabled = true;
2403
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);
2409
2410 y++;
2411 });
2412 }
2413
2414 // axis zoom
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);
2423 }
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);
2432 }
2433
2434 // marker
2435 if (this.marker.active) {
2436
2437 // round marker
2438 if (this.param.plot[0].type !== "colormap") {
2439 ctx.beginPath();
2440 ctx.globalAlpha = 0.1;
2441 ctx.arc(this.marker.sx, this.marker.sy, 10, 0, 2 * Math.PI);
2442 ctx.fillStyle = "#000000";
2443 ctx.fill();
2444 ctx.globalAlpha = 1;
2445
2446 ctx.beginPath();
2447 ctx.arc(this.marker.xs, this.marker.sy, 4, 0, 2 * Math.PI);
2448 ctx.fillStyle = "#000000";
2449 ctx.fill();
2450 }
2451
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);
2455
2456 // text label
2457 ctx.font = "12px sans-serif";
2458 ctx.textAlign = "left";
2459 let s;
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('('));
2464
2465 s = eval(f + "(this)");
2466 } else {
2467 if (this.param.plot[0].type === "bar")
2468 s = this.marker.x + " / " +
2469 this.marker.y.toPrecision(6).stripZeros();
2470 else
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());
2475 }
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;
2480
2481 // move marker inside if outside plotting area
2482 if (x + w >= this.x2)
2483 x = this.marker.sx - 10 - w;
2484
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);
2492 }
2493
2494 // call optional user overlay function
2495 if (this.param.overlay) {
2496
2497 // set default text
2498 ctx.textAlign = "left";
2499 ctx.textBaseline = "top";
2500 ctx.fillStyle = "black";
2501 ctx.strokeStyle = "black";
2502 ctx.font = "12px sans-serif";
2503
2504 eval(this.param.overlay + "(this, ctx)");
2505 }
2506
2507 //profile("end");
2508 }
2509
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)
2523 */
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];
2528
2529 if (xmin === undefined || xmax === undefined || isNaN(xmin) || isNaN(xmax))
2530 return;
2531
2532 if (xmax <= xmin || width <= 0)
2533 return;
2534
2535 ctx.textAlign = "center";
2536 ctx.textBaseline = "top";
2537
2538 if (logaxis) {
2539
2540 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
2541 if (isNaN(dx) || dx === 0) {
2542 xmin = 1E-20;
2543 dx = 1E-20;
2544 }
2545 label_dx = dx;
2546 major_dx = dx * 10;
2547 n_sig1 = 4;
2548
2549 } else { // linear axis ----
2550
2551 // use 10 as min tick distance
2552 dx = (xmax - xmin) / (width / 10);
2553
2554 int_dx = Math.floor(Math.log(dx) / LN10);
2555 frac_dx = Math.log(dx) / LN10 - int_dx;
2556
2557 if (frac_dx < 0) {
2558 frac_dx += 1;
2559 int_dx -= 1;
2560 }
2561
2562 tick_base = frac_dx < LOG2 ? 1 : frac_dx < LOG5 ? 2 : 3;
2563 major_base = label_base = tick_base + 1;
2564
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;
2569
2570 do {
2571 // number of significant digits
2572 if (xmin === 0)
2573 n_sig1 = 0;
2574 else
2575 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2576
2577 if (xmax === 0)
2578 n_sig2 = 0;
2579 else
2580 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / LN10) - Math.floor(Math.log(Math.abs(label_dx)) / LN10) + 1;
2581
2582 n_sig1 = Math.max(n_sig1, n_sig2);
2583
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);
2589
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;
2594
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);
2600
2601 // increasing label_dx, if labels would overlap
2602 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
2603 label_base++;
2604 label_dx = Math.pow(10, int_dx) * base[label_base];
2605 if (label_base % 3 === 2 && major_base % 3 === 1) {
2606 major_base++;
2607 major_dx = Math.pow(10, int_dx) * base[major_base];
2608 }
2609 } else
2610 break;
2611
2612 } while (true);
2613 }
2614
2615 x_act = Math.floor(xmin / dx) * dx;
2616
2617 ctx.strokeStyle = this.param.color.axis;
2618 ctx.drawLine(x1, y1, x1 + width, y1);
2619 let last_label_x = 0;
2620
2621 do {
2622 if (logaxis)
2623 x_screen = (Math.log(x_act) - Math.log(xmin)) /
2624 (Math.log(xmax) - Math.log(xmin)) * width + x1;
2625 else
2626 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
2627 xs = Math.floor(x_screen + 0.5);
2628
2629 if (x_screen > x1 + width + 0.001)
2630 break;
2631
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) {
2635
2636 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
2637 dx / label_dx / 10.0) {
2638 // label tick mark
2639 ctx.strokeStyle = this.param.color.axis;
2640 ctx.drawLine(xs, y1, xs, y1 + text);
2641
2642 // grid line
2643 if (grid !== 0 && xs > x1 && xs < x1 + width) {
2644 ctx.strokeStyle = this.param.color.grid;
2645 ctx.drawLine(xs, y1, xs, y1 + grid);
2646 }
2647
2648 // label
2649 if (label !== 0) {
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);
2657 }
2658 last_label_x = xs + ext.width / 2;
2659 }
2660 } else {
2661 // major tick mark
2662 ctx.strokeStyle = this.param.color.axis;
2663 ctx.drawLine(xs, y1, xs, y1 + major);
2664
2665 // grid line
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);
2669 }
2670 }
2671
2672 if (logaxis) {
2673 dx *= 10;
2674 major_dx *= 10;
2675 label_dx *= 10;
2676 }
2677
2678 } else {
2679 // minor tick mark
2680 ctx.strokeStyle = this.param.color.axis;
2681 ctx.drawLine(xs, y1, xs, y1 + minor);
2682 }
2683
2684 if (logaxis) {
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);
2689 }
2690
2691 // for log axis, also put labels on minor tick marks
2692 if (label !== 0) {
2693 let str;
2694 if (Math.abs(x_act) < 0.001 && Math.abs(x_act) > 1E-20)
2695 str = x_act.toExponential(n_sig1).stripZeros();
2696 else
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);
2705 }
2706
2707 last_label_x = xs + ext.width / 2;
2708 }
2709 }
2710 }
2711
2712 x_act += dx;
2713
2714 /* suppress 1.23E-17 ... */
2715 if (Math.abs(x_act) < dx / 100)
2716 x_act = 0;
2717
2718 } while (1);
2719 }
2720
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
2735 */
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];
2740
2741 if (ymin === undefined || ymax === undefined || isNaN(ymin) || isNaN(ymax))
2742 return;
2743
2744 if (ymax <= ymin || height <= 0)
2745 return;
2746
2747 if (label < 0)
2748 ctx.textAlign = "right";
2749 else
2750 ctx.textAlign = "left";
2751 ctx.textBaseline = "middle";
2752 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
2753
2754 if (!isFinite(ymax - ymin) || ymax === Number.MAX_VALUE) {
2755 dy = Number.MAX_VALUE / 10;
2756 label_dy = dy;
2757 major_dy = dy;
2758 n_sig1 = 1;
2759 } else if (logaxis) {
2760 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
2761 if (isNaN(dy) || dy === 0) {
2762 ymin = 1E-20;
2763 dy = 1E-20;
2764 }
2765 label_dy = dy;
2766 major_dy = dy * 10;
2767 n_sig1 = 4;
2768 } else {
2769 // use 6 as min tick distance
2770 dy = (ymax - ymin) / (height / 6);
2771
2772 int_dy = Math.floor(Math.log(dy) / Math.log(10));
2773 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
2774
2775 if (frac_dy < 0) {
2776 frac_dy += 1;
2777 int_dy -= 1;
2778 }
2779
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;
2782
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;
2787
2788 // number of significant digits
2789 if (ymin === 0)
2790 n_sig1 = 1;
2791 else
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;
2794
2795 if (ymax === 0)
2796 n_sig2 = 1;
2797 else
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;
2800
2801 n_sig1 = Math.max(n_sig1, n_sig2);
2802 n_sig1 = Math.max(1, n_sig1);
2803
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);
2811
2812 // increase label_dy if labels would overlap
2813 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
2814 label_base++;
2815 label_dy = Math.pow(10, int_dy) * base[label_base];
2816 if (label_base % 3 === 2 && major_base % 3 === 1) {
2817 major_base++;
2818 major_dy = Math.pow(10, int_dy) * base[major_base];
2819 }
2820 }
2821 }
2822
2823 y_act = Math.floor(ymin / dy) * dy;
2824
2825 let last_label_y = y1;
2826 let maxwidth = 0;
2827
2828 if (draw) {
2829 ctx.strokeStyle = this.param.color.axis;
2830 ctx.drawLine(x1, y1, x1, y1 - height);
2831 }
2832
2833 do {
2834 if (logaxis)
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;
2839 else
2840 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
2841 ys = Math.round(y_screen);
2842
2843 if (y_screen < y1 - height - 0.001 || isNaN(ys))
2844 break;
2845
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) {
2849
2850 if (Math.abs(Math.round(y_act / label_dy) - y_act / label_dy) <
2851 dy / label_dy / 10.0) {
2852 // label tick mark
2853 if (draw) {
2854 ctx.strokeStyle = this.param.color.axis;
2855 ctx.drawLine(x1, ys, x1 + text, ys);
2856 }
2857
2858 // grid line
2859 if (grid !== 0 && ys < y1 && ys > y1 - height)
2860 if (draw) {
2861 ctx.strokeStyle = this.param.color.grid;
2862 ctx.drawLine(x1, ys, x1 + grid, ys);
2863 }
2864
2865 // label
2866 if (label !== 0) {
2867 let str;
2868 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2869 str = y_act.toExponential(n_sig1).stripZeros();
2870 else
2871 str = y_act.toPrecision(n_sig1).stripZeros();
2872 maxwidth = Math.max(maxwidth, ctx.measureText(str).width);
2873 if (draw) {
2874 ctx.strokeStyle = this.param.color.label;
2875 ctx.fillStyle = this.param.color.label;
2876 ctx.fillText(str, x1 + label, ys);
2877 }
2878 last_label_y = ys - textHeight / 2;
2879 }
2880 } else {
2881 // major tick mark
2882 if (draw) {
2883 ctx.strokeStyle = this.param.color.axis;
2884 ctx.drawLine(x1, ys, x1 + major, ys);
2885 }
2886
2887 // grid line
2888 if (grid !== 0 && ys < y1 && ys > y1 - height)
2889 if (draw) {
2890 ctx.strokeStyle = this.param.color.grid;
2891 ctx.drawLine(x1, ys, x1 + grid, ys);
2892 }
2893 }
2894
2895 if (logaxis) {
2896 dy *= 10;
2897 major_dy *= 10;
2898 label_dy *= 10;
2899 }
2900
2901 } else {
2902 // minor tick mark
2903 if (draw) {
2904 ctx.strokeStyle = this.param.color.axis;
2905 ctx.drawLine(x1, ys, x1 + minor, ys);
2906 }
2907 }
2908
2909 if (logaxis) {
2910
2911 // for log axis, also put grid lines on minor tick marks
2912 if (grid !== 0 && ys < y1 && ys > y1 - height) {
2913 if (draw) {
2914 ctx.strokeStyle = this.param.color.grid;
2915 ctx.drawLine(x1+1, ys, x1 + grid - 1, ys);
2916 }
2917 }
2918
2919 // for log axis, also put labels on minor tick marks
2920 if (label !== 0) {
2921 let str;
2922 if (Math.abs(y_act) < 0.001 && Math.abs(y_act) > 1E-20)
2923 str = y_act.toExponential(n_sig1).stripZeros();
2924 else
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);
2930 if (draw) {
2931 ctx.strokeStyle = this.param.color.label;
2932 ctx.fillStyle = this.param.color.label;
2933 ctx.fillText(str, x1 + label, ys);
2934 }
2935 }
2936
2937 last_label_y = ys;
2938 }
2939 }
2940 }
2941
2942 y_act += dy;
2943
2944 // suppress 1.23E-17 ...
2945 if (Math.abs(y_act) < dy / 100)
2946 y_act = 0;
2947
2948 } while (1);
2949
2950 return maxwidth;
2951 };
2952
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
2966 */
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];
2970
2971 ctx.textAlign = "left";
2972 ctx.textBaseline = "top";
2973
2974 if (xmax <= xmin || width <= 0)
2975 return;
2976
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);
2981
2982 /* use 5 pixel as min tick distance */
2983 let dx = Math.round((xmax - xmin) / (width / 5));
2984
2985 let tick_base;
2986 for (tick_base = 0; base[tick_base]; tick_base++) {
2987 if (base[tick_base] > dx)
2988 break;
2989 }
2990 if (!base[tick_base])
2991 tick_base--;
2992 dx = base[tick_base];
2993
2994 let major_base = tick_base;
2995 let major_dx = dx;
2996
2997 let label_base = major_base;
2998 let label_dx = dx;
2999
3000 do {
3001 let str = ptimeToLabel(xmin, label_dx, forceDate);
3002 let maxWidth = ctx.measureText(str).width;
3003
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];
3008 else
3009 label_dx += 3600 * 24;
3010
3011 if (label_base > major_base + 1 || !base[label_base + 1]) {
3012 if (base[major_base + 1])
3013 major_dx = base[++major_base];
3014 else
3015 major_dx += 3600 * 24;
3016 }
3017
3018 if (major_base > tick_base + 1 || !base[label_base + 1]) {
3019 if (base[tick_base + 1])
3020 dx = base[++tick_base];
3021 else
3022 dx += 3600 * 24;
3023 }
3024
3025 } else
3026 break;
3027 } while (1);
3028
3029 let d = new Date(xmin * 1000);
3030 let tz = d.getTimezoneOffset() * 60;
3031
3032 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
3033
3034 ctx.strokeStyle = this.param.color.axis;
3035 ctx.drawLine(x1, y1, x1 + width, y1);
3036
3037 do {
3038 let xs = ((x_act - xmin) / (xmax - xmin) * width + x1);
3039
3040 if (xs > x1 + width + 0.001)
3041 break;
3042
3043 if (xs >= x1) {
3044 if ((x_act - tz) % major_dx === 0) {
3045 if ((x_act - tz) % label_dx === 0) {
3046 // label tick mark
3047 ctx.strokeStyle = this.param.color.axis;
3048 ctx.drawLine(xs, y1, xs, y1 + text);
3049
3050 // grid line
3051 if (grid !== 0 && xs > x1 && xs < x1 + width) {
3052 ctx.strokeStyle = this.param.color.grid;
3053 ctx.drawLine(xs, y1, xs, y1 + grid);
3054 }
3055
3056 // label
3057 if (label !== 0) {
3058 let str = ptimeToLabel(x_act, label_dx, forceDate);
3059
3060 // if labels at edge, shift them in
3061 let xl = xs - ctx.measureText(str).width / 2;
3062 if (xl < 0)
3063 xl = 0;
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);
3069 }
3070 } else {
3071 // major tick mark
3072 ctx.strokeStyle = this.param.color.axis;
3073 ctx.drawLine(xs, y1, xs, y1 + major);
3074 }
3075
3076 // grid line
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);
3080 }
3081 } else {
3082 // minor tick mark
3083 ctx.strokeStyle = this.param.color.axis;
3084 ctx.drawLine(xs, y1, xs, y1 + minor);
3085 }
3086 }
3087
3088 x_act += dx;
3089
3090 } while (1);
3091 };
3092
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
3103 */
3104 drawCAxis(ctx, x1, y1, width, tick, label, xmin, xmax, category, draw) {
3105 ctx.textBaseline = "middle";
3106
3107 if (width <= 0)
3108 return;
3109
3110 ctx.strokeStyle = this.param.color.axis;
3111
3112 if (draw)
3113 ctx.drawLine(x1, y1, x1 + width, y1);
3114
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;
3118 let maxWidth;
3119 let maxHeight;
3120 let angle;
3121 let x_offset = this.xMin0 - xmin + 0.5; // offset for tick translation
3122
3123 // determine tick positions
3124 for (angle = 0 ; angle < 90 ; angle += 10) {
3125 maxWidth = 0;
3126 maxHeight = 0;
3127 for (let i = 0; i < category.length; i++) {
3128
3129 // tick
3130 let shift = dx * (i + x_offset);
3131 if (draw && (shift >= 0) && (shift <= width)){
3132 ctx.drawLine(x1 + shift, y1, x1 + shift, y1 + tick);
3133 }
3134
3135 // label
3136 let w = ctx.measureText(category[i]).width;
3137 let h = this.param.xAxis.textSize;
3138
3139 const cos = Math.cos(angle/180*Math.PI);
3140 const sin = Math.sin(angle/180*Math.PI);
3141
3142 const width2 = Math.abs(w * cos) + Math.abs(h * sin);
3143 const height = Math.abs(w * sin) + Math.abs(h * cos);
3144
3145 maxWidth = Math.max(maxWidth, width2);
3146 maxHeight = Math.max(maxHeight, height);
3147 }
3148
3149 if (maxWidth * 1.1 < dx)
3150 break;
3151 }
3152
3153 this.param.xAxis.angle = angle;
3154 var voffset; // vertical offset for text label
3155 if (draw) {
3156
3157 // set vertical offset and text alignment based on angle
3158 if(angle > 20){
3159 voffset = 0;
3160 ctx.textAlign = "right";
3161 } else {
3162 var voffset = maxHeight / 2;
3163 ctx.textAlign = "center";
3164 }
3165
3166 // set tick labels
3167 ctx.fillStyle = this.param.color.label; // without this the text color matches the background unless the title is first set
3168
3169 for (let i = 0; i < category.length; i++) {
3170
3171 // check if the tick is within axis limits
3172 let shift = dx * (i + x_offset);
3173 if((shift >= 0) && (shift <= width)){
3174 ctx.save();
3175 ctx.translate(x1 + shift, y1 + label + voffset);
3176 ctx.rotate(-angle / 180 * Math.PI);
3177 ctx.fillText(category[i], 0, 0);
3178 ctx.restore();
3179 }
3180 }
3181 }
3182
3183 return maxHeight + 2;
3184 };
3185
3186 /** Download the figure as an image or data
3187 * @param {string} mode either "CSV" | "PNG"
3188 */
3189 download(mode) {
3190 let d = new Date();
3191 let filename = this.param.title.text + "-" +
3192 d.getFullYear() +
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);
3198
3199 // use trick from FileSaver.js
3200 let a = document.getElementById('downloadHook');
3201 if (a === null) {
3202 a = document.createElement("a");
3203 a.style.display = "none";
3204 a.id = "downloadHook";
3205 document.body.appendChild(a);
3206 }
3207
3208 if (mode === "CSV") {
3209 filename += ".csv";
3210
3211 let data = "";
3212
3213 // title
3214 this.param.plot.forEach(g => {
3215 if (g.type === "scatter" || g.type === "bar") {
3216 data += "X,";
3217 if (g.label === "")
3218 data += "Y";
3219 else
3220 data += g.label;
3221 data += '\n';
3222
3223 // data
3224 for (let i = 0; i < g.xData.length; i++) {
3225 data += g.xData[i] + ",";
3226 data += g.yData[i] + "\n";
3227 }
3228 data += '\n';
3229 }
3230
3231 if (g.type === "colormap") {
3232 data += "X \ Y,";
3233
3234 // X-header
3235 for (let i = 0; i < g.nx; i++)
3236 data += g.xData[i] + ",";
3237 data += '\n';
3238
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] + ",";
3243 data += '\n';
3244 }
3245 }
3246 });
3247
3248 let blob = new Blob([data], {type: "text/csv"});
3249 let url = window.URL.createObjectURL(blob);
3250
3251 a.href = url;
3252 a.download = filename;
3253 a.click();
3254 window.URL.revokeObjectURL(url);
3255 dlgAlert("Data downloaded to '" + filename + "'");
3256
3257 } else if (mode === "PNG") {
3258 filename += ".png";
3259
3260 let smb = this.param.showMenuButtons;
3261 this.param.showMenuButtons = false;
3262 this.draw();
3263
3264 try {
3265 let h = this;
3266 this.canvas.toBlob(function (blob) {
3267 let url = window.URL.createObjectURL(blob);
3268
3269 a.href = url;
3270 a.download = filename;
3271 a.click();
3272 window.URL.revokeObjectURL(url);
3273 dlgAlert("Image downloaded to '" + filename + "'");
3274
3275 h.param.showMenuButtons = smb;
3276 h.redraw();
3277
3278 }, 'image/png');
3279 } catch (e) {
3280 dlgAlert("Image download failed: " + e);
3281 this.param.showMenuButtons = smb;
3282 this.redraw();
3283 }
3284 }
3285
3286 };
3287
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
3293 */
3294 drawTextBox(ctx, text, x, y) {
3295 let line = text.split("\n");
3296
3297 let mw = 0;
3298 for (const g of line)
3299 if (ctx.measureText(g).width > mw)
3300 mw = ctx.measureText(g).width;
3301 let w = 5 + mw + 5;
3302 let h = parseInt(ctx.font) * 1.5;
3303
3304 let c = ctx.fillStyle;
3305 ctx.fillStyle = "white";
3306 ctx.fillRect(x, y, w, h * line.length);
3307 ctx.fillStyle = c;
3308 ctx.strokeRect(x, y, w, h * line.length);
3309
3310 for (let i=0 ; i<line.length ; i++)
3311 ctx.fillText(line[i], x+5, y + + 0.2*h + i*h);
3312 }
3313
3314 /** Handle mouse events
3315 * @param {Object} e mouse event object, specifies type, buttons
3316 */
3317 mouseEvent(e) {
3318 // execute callback if registered
3319 if (this.param.event) {
3320
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);
3335
3336 let flag = eval(this.param.event + "(e, this, ix, iy)");
3337 if (flag)
3338 return;
3339 }
3340 } else {
3341
3342 // call all other plots only with event and object
3343 let flag = eval(this.param.event + "(e, this)");
3344 if (flag)
3345 return;
3346
3347 }
3348 }
3349
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
3355 }
3356
3357 let cursor = "";
3358 let title = "";
3359 let cancel = false;
3360
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))
3364 cancel = true;
3365
3366 if (e.type === "mousedown") {
3367
3368 this.downloadSelector.style.display = "none";
3369
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 &&
3374 b.enabled) {
3375 b.click(this);
3376 }
3377 });
3378
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;
3392
3393 this.blockAutoScale = true;
3394 }
3395
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);
3402 }
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);
3408 }
3409
3410 } else if (cancel || e.type === "mouseup") {
3411
3412 if (this.drag.active)
3413 this.drag.active = false;
3414
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);
3420 if (x1 > x2)
3421 [x1, x2] = [x2, x1];
3422 this.xMin = x1;
3423 this.xMax = x2;
3424 }
3425 this.zoom.x.active = false;
3426 this.blockAutoScale = true;
3427 this.redraw();
3428 }
3429
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);
3435 if (y1 > y2)
3436 [y1, y2] = [y2, y1];
3437 this.yMin = y1;
3438 this.yMax = y2;
3439 }
3440 this.zoom.y.active = false;
3441 this.blockAutoScale = true;
3442 this.redraw();
3443 }
3444
3445 } else if (e.type === "mousemove") {
3446 if (this.drag.active) {
3447
3448 // execute dragging
3449 cursor = "move";
3450
3451 if (this.param.xAxis.log) {
3452 let dx = e.offsetX - this.drag.sxStart;
3453
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));
3456
3457 if (this.xMin <= 0)
3458 this.xMin = 1E-20;
3459 if (this.xMax <= 0)
3460 this.xMax = 1E-18;
3461 } else {
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;
3465 }
3466
3467 if (this.param.yAxis.log) {
3468 let dy = e.offsetY - this.drag.syStart;
3469
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));
3472
3473 if (this.yMin <= 0)
3474 this.yMin = 1E-20;
3475 if (this.yMax <= 0)
3476 this.yMax = 1E-18;
3477 } else {
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;
3481 }
3482
3483 this.redraw();
3484
3485 } else {
3486
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) {
3491 cursor = "pointer";
3492 title = b.title;
3493 }
3494 });
3495
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);
3500 this.redraw();
3501 }
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);
3505 this.redraw();
3506 }
3507
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)
3515 continue;
3516
3517 for (let i = 0; i < p.xData.length; i++) {
3518 let x;
3519 if (p.type === "bar")
3520 x = this.xToScreen(i + 0.5);
3521 else
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);
3526 if (d < minDist) {
3527 minDist = d;
3528 this.marker.x = p.xData[i];
3529 this.marker.y = p.yData[i];
3530 this.marker.sx = x;
3531 this.marker.sy = y;
3532 this.marker.mx = e.offsetX;
3533 this.marker.my = e.offsetY;
3534 this.marker.graphIndex = pi;
3535 this.marker.index = i;
3536 }
3537 }
3538 }
3539
3540 this.marker.active = Math.sqrt(minDist) < 10 && e.offsetX > this.x1 && e.offsetX < this.x2;
3541 }
3542
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);
3556
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];
3560
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;
3567 } else {
3568 this.marker.active = false;
3569 }
3570 }
3571
3572 this.draw();
3573 }
3574 }
3575
3576 } else if (e.type === "wheel" && this.param.mouseWheelZoom) {
3577
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)
3581 return;
3582
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);
3587
3588 let xMinOld = this.xMin;
3589 let xMaxOld = this.xMax;
3590 let yMinOld = this.yMin;
3591 let yMaxOld = this.yMax;
3592
3593 if (this.param.xAxis.log) {
3594
3595 scale *= 10;
3596 let f = (e.offsetX - this.x1) / (this.x2 - this.x1);
3597
3598 this.xMax *= 1 + scale * (1 - f);
3599 this.xMin /= 1 + scale * f;
3600
3601 if (this.xMax <= this.xMin) {
3602 this.xMin = xMinOld;
3603 this.xMax = xMaxOld;
3604 }
3605
3606 } else {
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);
3611 }
3612
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;
3617 }
3618
3619 if (this.param.yAxis.log) {
3620
3621 scale *= 10;
3622 let f = (e.offsetY - this.y2) / (this.y1 - this.y2);
3623 let yMinOld = this.yMin;
3624 let yMaxOld = this.yMax;
3625
3626 this.yMax *= 1 + scale * f;
3627 this.yMin /= 1 + scale * (1 - f);
3628
3629 if (this.yMax <= this.yMin) {
3630 this.yMin = yMinOld;
3631 this.yMax = yMaxOld;
3632 }
3633
3634 } else {
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);
3639 }
3640
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;
3645 }
3646
3647 this.blockAutoScale = true;
3648
3649 this.draw();
3650 e.preventDefault();
3651 }
3652
3653 this.parentDiv.title = title;
3654 this.parentDiv.style.cursor = cursor;
3655 }
3656
3657 /** Reset min/max of x and y axes, redraws */
3658 resetAxes() {
3659 this.xMin = this.xMin0;
3660 this.xMax = this.xMax0;
3661 this.yMin = this.yMin0;
3662 this.yMax = this.yMax0;
3663 this.blockAutoScale = false;
3664 this.redraw();
3665 }
3666}