svelte-plots-basic 2.5.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +530 -445
- package/package.json +5 -2
- package/src/2d/Area.svelte +56 -48
- package/src/2d/Axes.svelte +409 -373
- package/src/2d/AxisLines.svelte +37 -36
- package/src/2d/AxisTickLabels.svelte +41 -49
- package/src/2d/Bars.svelte +102 -47
- package/src/2d/Box.svelte +21 -41
- package/src/2d/ColormapLegend.svelte +47 -111
- package/src/2d/Heatmap.svelte +107 -85
- package/src/2d/Legend.svelte +115 -126
- package/src/2d/Lines.svelte +54 -40
- package/src/2d/Multilines.svelte +73 -46
- package/src/2d/Points.svelte +76 -58
- package/src/2d/Rectangles.svelte +67 -58
- package/src/2d/Segments.svelte +59 -46
- package/src/2d/TextLabels.svelte +102 -72
- package/src/2d/TextLegend.svelte +17 -39
- package/src/2d/XAxis.svelte +49 -98
- package/src/2d/YAxis.svelte +47 -93
- package/src/2d/index.js +2 -1
- package/src/3d/Axes.svelte +306 -348
- package/src/3d/AxisLines.svelte +19 -37
- package/src/3d/AxisTickLabels.svelte +15 -32
- package/src/3d/Lines.svelte +59 -32
- package/src/3d/Mesh.svelte +106 -128
- package/src/3d/Points.svelte +56 -36
- package/src/3d/Segments.svelte +68 -52
- package/src/3d/TextLabels.svelte +63 -49
- package/src/3d/XAxis.svelte +36 -113
- package/src/3d/YAxis.svelte +36 -113
- package/src/3d/ZAxis.svelte +37 -114
- package/src/3d/index.js +2 -1
- package/src/constants.js +103 -0
- package/src/methods.js +1484 -0
- package/src/2d/Axis.svelte +0 -60
- package/src/3d/Axis.svelte +0 -64
- package/src/Colors.js +0 -51
- package/src/Utils.js +0 -626
package/src/methods.js
ADDED
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
import { cbind, vector, isvector, Vector, matrix } from 'mdatools/arrays';
|
|
2
|
+
import { min, max, diff } from 'mdatools/stat';
|
|
3
|
+
import { TICK_NUM, XTICK_NUM, YTICK_NUM, TICK_SIZE, COLMAP_WIDTH_RATIO, COLMAP_EL_HEIGHTS_PX, COLMAP_TOP_MARGIN } from './constants';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates array with sequence of values from 1 to n
|
|
8
|
+
* @param {number} n - length of array.
|
|
9
|
+
* @returns {Array}
|
|
10
|
+
*/
|
|
11
|
+
export function seq(n) {
|
|
12
|
+
let x = Array(n);
|
|
13
|
+
for (let i = 0; i < n; i++) {
|
|
14
|
+
x[i] = i + 1;
|
|
15
|
+
}
|
|
16
|
+
return x;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check of array has correct type, does not contaim undefined or NaN values and has proper length.
|
|
22
|
+
* @param {Array} arr - array to check.
|
|
23
|
+
* @param {number} len - expected length.
|
|
24
|
+
* @returns {boolean} check result.
|
|
25
|
+
*/
|
|
26
|
+
export function checkArray(arr, len) {
|
|
27
|
+
return Array.isArray(arr) &&
|
|
28
|
+
arr.length === len &&
|
|
29
|
+
!arr.some(v => v === undefined) &&
|
|
30
|
+
!arr.some(v => isNaN(v));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generic function to transform x or y-values from plot coordinates to screen (SVG) coordinates.
|
|
36
|
+
*
|
|
37
|
+
* @param {Array|Vector} v - vector with coordinates (or objects size) in original plot coordinates.
|
|
38
|
+
* @param {Object} tA - array with scaling and translation factors.
|
|
39
|
+
*
|
|
40
|
+
* @returns {Vector} vector with rescaled values
|
|
41
|
+
*
|
|
42
|
+
*/
|
|
43
|
+
export function transform(v, tA) {
|
|
44
|
+
if (v === undefined || v === null) return undefined;
|
|
45
|
+
if (Array.isArray(v)) v = vector(v);
|
|
46
|
+
if (!isvector(v) || v.length < 1) return undefined;
|
|
47
|
+
return v.apply(a => roundCoords((a - tA[1]) * tA[0] + tA[2])).v;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// shortcut for transformation of coordinates
|
|
52
|
+
export function transformCoords(v, t) {
|
|
53
|
+
return transform(v, t.coords);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// shortcut for transformation of objects
|
|
57
|
+
export function transformObjects(v, t) {
|
|
58
|
+
return transform(v, t.objects);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generic function to transform x or y-values from screen (SVG) coordinates to plot coordinates.
|
|
64
|
+
*
|
|
65
|
+
* @param {Array|Vector} v - vector with coordinates (or objects size) in screen (SVG) coordinates.
|
|
66
|
+
* @param {Object} tA - array with scaling and translation factors.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Vector} vector plot coordinates.
|
|
69
|
+
*
|
|
70
|
+
*/
|
|
71
|
+
export function invTransform(v, tA) {
|
|
72
|
+
if (v === undefined || v === null) return undefined;
|
|
73
|
+
if (Array.isArray(v)) v = vector(v);
|
|
74
|
+
if (!isvector(v) || v.length < 1) return undefined;
|
|
75
|
+
return v.apply(a => (a - tA[2]) / tA[0] + tA[1]).v;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// shortcut for inverse transformation of coordinates
|
|
79
|
+
export function invTransformCoords(v, t) {
|
|
80
|
+
return invTransform(v, t.coords);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// shortcut for inverse transformation of objects
|
|
84
|
+
export function invTransformObjects(v, t) {
|
|
85
|
+
return invTransform(v, t.objects);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates label for a tick factor.
|
|
91
|
+
*
|
|
92
|
+
* @param {Number} tf - tick factor value (negative or positive)
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
export function getTickFactorLabel(tf) {
|
|
96
|
+
if (tf === 0) return '';
|
|
97
|
+
return `<tspan>×10</tspan><tspan font-size="0.75em" dy="-0.5em">${(tf).toString()}</tspan>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Trim number leaving only significant decimals.
|
|
103
|
+
* @param {number} num
|
|
104
|
+
* @returns trimmed number
|
|
105
|
+
*/
|
|
106
|
+
export function trimNum(num) {
|
|
107
|
+
|
|
108
|
+
// check if number is whole — return as is
|
|
109
|
+
if (num === Math.round(num)) return num;
|
|
110
|
+
|
|
111
|
+
// if it is very large remove the decimals
|
|
112
|
+
if (Math.abs(num) > 10**6) return Math.round(num);
|
|
113
|
+
|
|
114
|
+
// if it is very small return as is
|
|
115
|
+
if (Math.abs(num) < 10**(-6)) return num;
|
|
116
|
+
|
|
117
|
+
// Convert to string with precision to maintain significant digits
|
|
118
|
+
let str = num.toPrecision(10);
|
|
119
|
+
|
|
120
|
+
// Remove trailing zeros
|
|
121
|
+
str = str.replace(/\.?0+$/, '');
|
|
122
|
+
|
|
123
|
+
// Convert back to number
|
|
124
|
+
return parseFloat(str);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate vector with axis tick labels and tick factor based on numeric tick values.
|
|
130
|
+
*
|
|
131
|
+
* @param {Vector} ticks - vector with numeric tick values.
|
|
132
|
+
*
|
|
133
|
+
* @returns {Array} an array with tick factor and array with tick labels.
|
|
134
|
+
*/
|
|
135
|
+
export function getTickLabels(ticks) {
|
|
136
|
+
|
|
137
|
+
if (ticks === undefined) return [0, undefined];
|
|
138
|
+
if (ticks.length < 1) return [0, undefined];
|
|
139
|
+
if (ticks.length === 1) return [0, [ticks[0].toString()]];
|
|
140
|
+
|
|
141
|
+
// compute and correct step between ticks
|
|
142
|
+
let step = trimNum(Math.abs(min(diff(ticks))));
|
|
143
|
+
let tickFactor = 0;
|
|
144
|
+
|
|
145
|
+
// step is large (over 1)
|
|
146
|
+
if (step >= 1) {
|
|
147
|
+
if (step < 100) return [0, Array.from(ticks).map(v => v.toFixed(0))];
|
|
148
|
+
let digNum = Math.ceil(trimNum(Math.log10(step)));
|
|
149
|
+
tickFactor = digNum - 2;
|
|
150
|
+
return [tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toFixed(0))];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let decNum = Math.ceil(-trimNum(Math.log10(step)));
|
|
154
|
+
if (decNum <= 2) {
|
|
155
|
+
return [0, Array.from(ticks).map(v => v.toFixed(decNum))];
|
|
156
|
+
}
|
|
157
|
+
tickFactor = decNum - 2;
|
|
158
|
+
decNum = 2;
|
|
159
|
+
|
|
160
|
+
if (trimNum(Math.abs(max(ticks))) <= 10) {
|
|
161
|
+
return [-tickFactor, Array.from(ticks).map(v => (v * Math.pow(10, tickFactor)).toFixed(decNum))];
|
|
162
|
+
} else {
|
|
163
|
+
return [-tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toString())];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check coordinates and convert them to vector if necessary.
|
|
170
|
+
*
|
|
171
|
+
* @param {Array|Vector} x - vector or array with coordinates.
|
|
172
|
+
* @param {string} source - name of component which called the method (needed for error message).
|
|
173
|
+
*
|
|
174
|
+
* @returns {Vector}
|
|
175
|
+
*
|
|
176
|
+
*/
|
|
177
|
+
export function checkCoords(x, source, len) {
|
|
178
|
+
|
|
179
|
+
if (Array.isArray(x)) {
|
|
180
|
+
x = vector(x);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!isvector(x)) {
|
|
184
|
+
console.error(source + ': coordinates must be provided as array or instance of Vector class.');
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (x.length < 1) {
|
|
189
|
+
console.error(source + ': vector with coordinates is empty.');
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (len && x.length !== len) {
|
|
194
|
+
console.error(source + ': vector does not have the expected length of ' + len + '.');
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return x;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create string with coordinates of SVG polygon.
|
|
204
|
+
*
|
|
205
|
+
* @param {Vector} x - vector with x-coordinates of polygon points.
|
|
206
|
+
* @param {Vector} y - vector with y-coordinates of polygon points.
|
|
207
|
+
* @param {Array} tX - transformation parameters for x-coordinates (from 'Axes').
|
|
208
|
+
* @param {Array} tY - transformation parameters for y-coordinates (from 'Axes').
|
|
209
|
+
* @param {Object} axes - JSON with Axes context.
|
|
210
|
+
*
|
|
211
|
+
* @returns {string} string with coordinates.
|
|
212
|
+
*
|
|
213
|
+
*/
|
|
214
|
+
export function val2p(x, y, tX, tY) {
|
|
215
|
+
|
|
216
|
+
if (!x || !y || x.length !== y.length) {
|
|
217
|
+
console.error('val2p: provided coordinate values are not correct.');
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const tXc = tX.coords;
|
|
222
|
+
const tYc = tY.coords;
|
|
223
|
+
let p = '';
|
|
224
|
+
for (let i = 0; i < x.length; i++) {
|
|
225
|
+
const px = roundCoords((x.v[i] - tXc[1]) * tXc[0] + tXc[2]);
|
|
226
|
+
const py = roundCoords((y.v[i] - tYc[1]) * tYc[0] + tYc[2]);
|
|
227
|
+
p += px + ',' + py + ' ';
|
|
228
|
+
}
|
|
229
|
+
return p;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validate tick labels entered by user and process them to SVG markup.
|
|
235
|
+
*
|
|
236
|
+
* @param {Array} tickLabels - array with tick labels.
|
|
237
|
+
* @param {Vector} ticks - vector with ticks (already validated).
|
|
238
|
+
* @returns {Array} - first element are processed tick labels, second - error message if any.
|
|
239
|
+
*/
|
|
240
|
+
export function validateTickLabels(tickLabels, ticks) {
|
|
241
|
+
|
|
242
|
+
if (!tickLabels) {
|
|
243
|
+
return [null, ''];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!Array.isArray(tickLabels)) {
|
|
247
|
+
return [null, 'tick labels must be provided as array.'];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!ticks || ticks.length < 1) {
|
|
251
|
+
return [null, 'tick labels must be accompanied with tick values.']
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (ticks.v.length !== tickLabels.length) {
|
|
255
|
+
return [null, 'number of tick labels and values should be the same.'];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [tickLabels.map(v => text2svg(v)), ''];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Validate tick values entered by user.
|
|
263
|
+
*
|
|
264
|
+
* @param {Array|Vector} ticks - array of vector with tick values.
|
|
265
|
+
* @param {Array} lim - array with axis limits.
|
|
266
|
+
* @returns {Array} - first element are processed ticks, second - error message if any.
|
|
267
|
+
*/
|
|
268
|
+
export function validateTicks(ticks, lim) {
|
|
269
|
+
|
|
270
|
+
if (Array.isArray(ticks)) {
|
|
271
|
+
ticks = vector(ticks);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!isvector(ticks)) {
|
|
275
|
+
return [null, 'tick values must be provided as an array or as a vector.'];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (ticks.v.some(v => isNaN(v))) {
|
|
279
|
+
return [null, 'tick values must be numeric'];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const un = new Set(ticks.v);
|
|
283
|
+
if (ticks.length !== un.size) {
|
|
284
|
+
return [null, 'ticks values must be unique.'];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// check if provided ticks values are outside the axis limit range continue with automatic ticks
|
|
288
|
+
const newTicks = ticks.filter(x => x >= lim[0] & x <= lim[1]);
|
|
289
|
+
if (newTicks.length !== ticks.length) {
|
|
290
|
+
return [null, 'some of ticks values are outside axis limits.'];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return [newTicks, ''];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Computes nice tick values for axis.
|
|
299
|
+
*
|
|
300
|
+
* @param {Array} ticks - vector with ticks if alredy available (if not, new will be computed).
|
|
301
|
+
* @param {Array} lim - vector with axis limits tickets must be computed for.
|
|
302
|
+
* @param {number} maxTickNum - maximum number of ticks to compute.
|
|
303
|
+
* @param {boolean} round - round or not the fractions when computing nice numbers for the ticks.
|
|
304
|
+
* @param {boolean} whole - should the ticks be a whole number.
|
|
305
|
+
* @param {Number} deltaFactor - percent of range to use as internal margins
|
|
306
|
+
*
|
|
307
|
+
* @returns {Array} an array with computed tick positions.
|
|
308
|
+
*
|
|
309
|
+
*/
|
|
310
|
+
export function getAxisTicks(ticks, lim, maxTickNum, round, whole, deltaFactor) {
|
|
311
|
+
|
|
312
|
+
if (round === undefined || round === null) {
|
|
313
|
+
round = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (whole === undefined || whole === null) {
|
|
317
|
+
whole = false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (deltaFactor === undefined) {
|
|
321
|
+
deltaFactor = 0.0001;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// if ticks are already provided do not recompute them
|
|
325
|
+
// the validation is done in corresponding component, so no need to do it again
|
|
326
|
+
if (ticks) return ticks;
|
|
327
|
+
|
|
328
|
+
// check if limits are ok
|
|
329
|
+
if (typeof(lim) !== "object" || lim[0] === undefined || lim[1] === undefined) {
|
|
330
|
+
console.error('getAxisTicks: provided axis limits are not valid.');
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// get range as a nice number and compute min, max and steps for the tick sequence
|
|
335
|
+
const delta = Math.abs((lim[1] - lim[0])) * deltaFactor;
|
|
336
|
+
const localRange = lim[1] - lim[0] - 2 * delta;
|
|
337
|
+
const exponent = Math.floor(Math.log10(localRange));
|
|
338
|
+
const fraction = 1 / Math.pow(10, exponent - 1);
|
|
339
|
+
const range = Math.round(localRange * fraction) / fraction;
|
|
340
|
+
|
|
341
|
+
let tickSpacing = niceNum(range / maxTickNum, false);
|
|
342
|
+
if (whole) {
|
|
343
|
+
if (Math.abs(lim[1] - lim[0]) <= 1) {
|
|
344
|
+
console.error('getAxisTicks: parameter "whole" will be ignored as axis limits are too narrow to fit whole ticks.');
|
|
345
|
+
} else {
|
|
346
|
+
if (tickSpacing < 1) tickSpacing = 1;
|
|
347
|
+
tickSpacing = Math.round(tickSpacing);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// compute smallest and largest tick rounded to tickSpacing
|
|
352
|
+
let tickMin = Math.ceil((lim[0] + delta ) / tickSpacing) * tickSpacing;
|
|
353
|
+
let tickMax = Math.floor((lim[1] - delta) / tickSpacing) * tickSpacing;
|
|
354
|
+
|
|
355
|
+
// adjust largest tick if it is too close to axis limit and tick spacing
|
|
356
|
+
if (Math.abs((lim[0] - tickMin)) < (tickSpacing / 10)) tickMin = tickMin + tickSpacing;
|
|
357
|
+
if (Math.abs((lim[1] - tickMax)) < (tickSpacing / 10)) tickMax = tickMax - tickSpacing;
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// recompute maxTickNum
|
|
361
|
+
maxTickNum = (Math.ceil(trimNum((tickMax - tickMin) / tickSpacing)) + 1);
|
|
362
|
+
|
|
363
|
+
// create a sequence of ticks
|
|
364
|
+
ticks = Vector.zeros(maxTickNum);
|
|
365
|
+
for (let i = 0; i < maxTickNum; i++) {
|
|
366
|
+
ticks.v[i] = tickMin + tickSpacing * i;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// if step is smaller than 1 round values to remove small decimals accidentially added by JS
|
|
370
|
+
if (Math.abs(tickSpacing) < 1) {
|
|
371
|
+
const decNum = Math.round(-Math.log10(tickSpacing));
|
|
372
|
+
const r = Math.pow(10, 1 + Math.round(-Math.log10(tickSpacing)));
|
|
373
|
+
ticks = ticks.apply(v => (Math.round((v + Number.EPSILON) * r) / r));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// make sure the ticks are not aligned with axes limits
|
|
377
|
+
return ticks.filter(x => x >= lim[0] & x <= lim[1]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Computes a nice spacing value for a given range.
|
|
383
|
+
*
|
|
384
|
+
* @param {Number} localRange - a range (max - min).
|
|
385
|
+
* @param {boolean} round - round or not the fractions when computing the number.
|
|
386
|
+
*
|
|
387
|
+
* @returns {Number} the computed spacing value.
|
|
388
|
+
*
|
|
389
|
+
*/
|
|
390
|
+
export function niceNum(localRange, round) {
|
|
391
|
+
|
|
392
|
+
const exponent = Math.floor(Math.log10(localRange));
|
|
393
|
+
const fraction = localRange / Math.pow(10, exponent);
|
|
394
|
+
let niceFraction;
|
|
395
|
+
|
|
396
|
+
if (round) {
|
|
397
|
+
if (fraction < 1.5)
|
|
398
|
+
niceFraction = 1;
|
|
399
|
+
else if (fraction < 3)
|
|
400
|
+
niceFraction = 2;
|
|
401
|
+
else if (fraction < 7)
|
|
402
|
+
niceFraction = 5;
|
|
403
|
+
else
|
|
404
|
+
niceFraction = 10;
|
|
405
|
+
} else {
|
|
406
|
+
if (fraction <= 1)
|
|
407
|
+
niceFraction = 1;
|
|
408
|
+
else if (fraction <= 2)
|
|
409
|
+
niceFraction = 2;
|
|
410
|
+
else if (fraction <= 5)
|
|
411
|
+
niceFraction = 5;
|
|
412
|
+
else
|
|
413
|
+
niceFraction = 10;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return niceFraction * Math.pow(10, exponent);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Rounds coordinate values to single decimal
|
|
422
|
+
*/
|
|
423
|
+
export function roundCoords(x) {
|
|
424
|
+
return Math.round(x * 10) / 10
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Computes a scale level.
|
|
430
|
+
*
|
|
431
|
+
* @param {numeric} width - width of plotting area in pixels.
|
|
432
|
+
* @param {numeric} height - height of plotting area in pixels.
|
|
433
|
+
*
|
|
434
|
+
* @returns {text} the scale level ("small", "medium" or "large").
|
|
435
|
+
*
|
|
436
|
+
*/
|
|
437
|
+
export function getScale(width, height) {
|
|
438
|
+
if (height < 300.2 || width < 400.2) return "small";
|
|
439
|
+
if (height < 400.2 || width < 700.2) return "medium";
|
|
440
|
+
if (height < 650.2 || width < 850.2) return "large";
|
|
441
|
+
return "xlarge";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Computes a scale level for single axis.
|
|
447
|
+
*
|
|
448
|
+
* @param {numeric} size - width or height of plotting area in pixels.
|
|
449
|
+
* @returns {text} the scale level ("small", "medium" or "large").
|
|
450
|
+
*
|
|
451
|
+
*/
|
|
452
|
+
export function getAxisScale(width, size) {
|
|
453
|
+
if (width < 400.2) return "small";
|
|
454
|
+
if (width < 700.2) return "medium";
|
|
455
|
+
if (width < 900.2) return "large";
|
|
456
|
+
return "xlarge";
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
/*************************************************************/
|
|
461
|
+
/* Functions needed to adjust DPI of resulting PNG image */
|
|
462
|
+
/* taken from: https://github.com/shutterstock/changeDPI */
|
|
463
|
+
/* and modified */
|
|
464
|
+
/*************************************************************/
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Create table for PNG CRC calculation
|
|
468
|
+
*/
|
|
469
|
+
function createPngDataTable() {
|
|
470
|
+
const crcTable = new Int32Array(256);
|
|
471
|
+
for (let n = 0; n < 256; n++) {
|
|
472
|
+
let c = n;
|
|
473
|
+
for (let k = 0; k < 8; k++) {
|
|
474
|
+
c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
475
|
+
}
|
|
476
|
+
crcTable[n] = c;
|
|
477
|
+
}
|
|
478
|
+
return crcTable;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Calculate CRC for PNG image
|
|
484
|
+
* @param buf image data
|
|
485
|
+
*/
|
|
486
|
+
function calcCrc(buf) {
|
|
487
|
+
let c = -1;
|
|
488
|
+
let pngDataTable = createPngDataTable();
|
|
489
|
+
for (let n = 0; n < buf.length; n++) {
|
|
490
|
+
c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
|
|
491
|
+
}
|
|
492
|
+
return c ^ -1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Change DPI of PNG image
|
|
498
|
+
* @param dataArray image data
|
|
499
|
+
* @param dpi new DPI
|
|
500
|
+
*/
|
|
501
|
+
function changeDpiOnArray(dataArray, dpi) {
|
|
502
|
+
|
|
503
|
+
const physChunk = new Uint8Array(13);
|
|
504
|
+
// chunk header pHYs
|
|
505
|
+
// 9 bytes of data
|
|
506
|
+
// 4 bytes of crc
|
|
507
|
+
// this multiplication is because the standard is dpi per meter.
|
|
508
|
+
dpi *= 39.3701;
|
|
509
|
+
physChunk[0] = 'p'.charCodeAt(0);
|
|
510
|
+
physChunk[1] = 'H'.charCodeAt(0);
|
|
511
|
+
physChunk[2] = 'Y'.charCodeAt(0);
|
|
512
|
+
physChunk[3] = 's'.charCodeAt(0);
|
|
513
|
+
physChunk[4] = dpi >>> 24; // dpiX byte 3
|
|
514
|
+
physChunk[5] = dpi >>> 16; // dpiX byte 2
|
|
515
|
+
physChunk[6] = dpi >>> 8; // dpiX byte 1
|
|
516
|
+
physChunk[7] = dpi & 0xff; // dpiX byte 0
|
|
517
|
+
physChunk[8] = physChunk[4]; // dpiY byte 3
|
|
518
|
+
physChunk[9] = physChunk[5]; // dpiY byte 2
|
|
519
|
+
physChunk[10] = physChunk[6]; // dpiY byte 1
|
|
520
|
+
physChunk[11] = physChunk[7]; // dpiY byte 0
|
|
521
|
+
physChunk[12] = 1; // dot per meter....
|
|
522
|
+
|
|
523
|
+
// compute and add CRC for physChunk
|
|
524
|
+
const crc = calcCrc(physChunk);
|
|
525
|
+
const crcChunk = new Uint8Array(4);
|
|
526
|
+
crcChunk[0] = crc >>> 24;
|
|
527
|
+
crcChunk[1] = crc >>> 16;
|
|
528
|
+
crcChunk[2] = crc >>> 8;
|
|
529
|
+
crcChunk[3] = crc & 0xff;
|
|
530
|
+
|
|
531
|
+
// chunk structur 4 bytes for length is 9
|
|
532
|
+
const chunkLength = new Uint8Array(4);
|
|
533
|
+
chunkLength[0] = 0;
|
|
534
|
+
chunkLength[1] = 0;
|
|
535
|
+
chunkLength[2] = 0;
|
|
536
|
+
chunkLength[3] = 9;
|
|
537
|
+
|
|
538
|
+
const finalHeader = new Uint8Array(54);
|
|
539
|
+
finalHeader.set(dataArray, 0);
|
|
540
|
+
finalHeader.set(chunkLength, 33);
|
|
541
|
+
finalHeader.set(physChunk, 37);
|
|
542
|
+
finalHeader.set(crcChunk, 50);
|
|
543
|
+
return finalHeader;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Change DPI of PNG image
|
|
549
|
+
* @param base64Image image data
|
|
550
|
+
* @param dpi new DPI
|
|
551
|
+
*/
|
|
552
|
+
function changeDPI(base64Image, dpi) {
|
|
553
|
+
|
|
554
|
+
const dataSplitted = base64Image.split(',');
|
|
555
|
+
const format = dataSplitted[0];
|
|
556
|
+
const body = dataSplitted[1];
|
|
557
|
+
|
|
558
|
+
// here we assume there is pHYS chunk in the header
|
|
559
|
+
const headerLength = 33 / 3 * 4;
|
|
560
|
+
|
|
561
|
+
const stringHeader = body.substring(0, headerLength);
|
|
562
|
+
const restOfData = body.substring(headerLength);
|
|
563
|
+
const headerBytes = atob(stringHeader);
|
|
564
|
+
const dataArray = new Uint8Array(headerBytes.length);
|
|
565
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
566
|
+
dataArray[i] = headerBytes.charCodeAt(i);
|
|
567
|
+
}
|
|
568
|
+
const finalArray = changeDpiOnArray(dataArray, dpi, 'image/png');
|
|
569
|
+
const base64Header = btoa(String.fromCharCode(...finalArray));
|
|
570
|
+
return [format, ',', base64Header, restOfData].join('');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Downloads plot (or any other SVG image) as a PNG file.
|
|
576
|
+
*
|
|
577
|
+
* @param {SVGElement} svg - root SVG element of a plot
|
|
578
|
+
* @param {string} fileName - filename to save with (without extension)
|
|
579
|
+
* @param {number} width - desired image width in cm
|
|
580
|
+
* @param {number} height - desired image height in cm
|
|
581
|
+
* @param {number} res - resolution (pixels per inch)
|
|
582
|
+
*
|
|
583
|
+
*/
|
|
584
|
+
export function downloadPNG (svg, fileName, width, height, res) {
|
|
585
|
+
|
|
586
|
+
if (!width) width = 10;
|
|
587
|
+
if (!height) height = 10;
|
|
588
|
+
if (!res) res = 300;
|
|
589
|
+
|
|
590
|
+
if (width < 1 || width > 30) {
|
|
591
|
+
console.error('Parameter "width" must be between 1 and 30 (cm).');
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (height < 1 || height > 30) {
|
|
596
|
+
console.error('Parameter "height" must be between 1 and 30 (cm).');
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (res < 50 || res > 1200) {
|
|
601
|
+
console.error('Parameter "res" must be between 50 and 1200 (ppi).');
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!svg || !(svg instanceof SVGElement)) {
|
|
606
|
+
console.error('Parameter "svg" is not an instance of SVGElement.');
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!fileName || fileName.trim() === '') {
|
|
611
|
+
fileName = 'plot';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
// recalculate width and height to pixels
|
|
616
|
+
const pxWidth = width / 2.54 * res;
|
|
617
|
+
const pxHeight = height / 2.54 * res;
|
|
618
|
+
|
|
619
|
+
setTimeout(() => {
|
|
620
|
+
|
|
621
|
+
// compute canvas size based on SVG size and desired PNG WIDTH in pixels
|
|
622
|
+
const svgHeight = svg.clientHeight;
|
|
623
|
+
const svgWidth = svg.clientWidth;
|
|
624
|
+
let scaleFactor = Math.max(pxWidth / svgWidth, pxHeight / svgHeight);
|
|
625
|
+
|
|
626
|
+
// this is needed if SVG image is larger than the future PNG image
|
|
627
|
+
let specialScale = 1;
|
|
628
|
+
if (scaleFactor < 1) {
|
|
629
|
+
specialScale = scaleFactor;
|
|
630
|
+
scaleFactor = 1;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// create a new canvas element
|
|
634
|
+
const canvas = document.createElement('canvas');
|
|
635
|
+
const context = canvas.getContext('2d');
|
|
636
|
+
|
|
637
|
+
// set the canvas size to match the SVG element
|
|
638
|
+
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
639
|
+
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
640
|
+
|
|
641
|
+
// set corresponding attributes for SVG element
|
|
642
|
+
svg.setAttribute('width', svgWidth * scaleFactor);
|
|
643
|
+
svg.setAttribute('height', svgHeight * scaleFactor);
|
|
644
|
+
|
|
645
|
+
// draw a white background
|
|
646
|
+
context.fillStyle = 'white';
|
|
647
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
648
|
+
context.scale(scaleFactor, scaleFactor)
|
|
649
|
+
|
|
650
|
+
// draw the SVG onto the canvas
|
|
651
|
+
const svgXml = new XMLSerializer().serializeToString(svg);
|
|
652
|
+
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
653
|
+
const url = URL.createObjectURL(blob);
|
|
654
|
+
|
|
655
|
+
const img = new Image();
|
|
656
|
+
|
|
657
|
+
img.onload = function () {
|
|
658
|
+
|
|
659
|
+
// downscale image if it must be smaller than the current SVG element
|
|
660
|
+
if (specialScale < 0) {
|
|
661
|
+
img.width = img.width * specialScale;
|
|
662
|
+
img.height = img.height * specialScale;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
666
|
+
|
|
667
|
+
// create a data URL for the canvas and adjust its DPI
|
|
668
|
+
const dataURL = canvas.toDataURL('image/png');
|
|
669
|
+
const dataURL2 = changeDPI(dataURL, res);
|
|
670
|
+
|
|
671
|
+
// create a temporary link and trigger the download
|
|
672
|
+
const downloadLink = document.createElement('a');
|
|
673
|
+
downloadLink.href = dataURL2;
|
|
674
|
+
downloadLink.download = `${fileName}.png`;
|
|
675
|
+
downloadLink.click();
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// set the image source
|
|
679
|
+
img.src = url;
|
|
680
|
+
}, 20);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Downloads plot (or any other SVG image) as an SVG file.
|
|
687
|
+
*
|
|
688
|
+
* @param {SVGElement} svg - root SVG element of a plot
|
|
689
|
+
* @param {string} fileName - filename to save with (without extension)
|
|
690
|
+
*/
|
|
691
|
+
export function downloadSVG(svg, fileName) {
|
|
692
|
+
|
|
693
|
+
if (!svg || !(svg instanceof SVGElement)) {
|
|
694
|
+
console.error('Variable "svg" is not an instance of SVGElement.');
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!fileName || fileName.trim() === "") {
|
|
699
|
+
fileName = "plot";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const svgHeight = svg.clientHeight;
|
|
703
|
+
const svgWidth = svg.clientWidth;
|
|
704
|
+
svg.setAttribute('width', svgWidth + 'px');
|
|
705
|
+
svg.setAttribute('height', svgHeight + 'px');
|
|
706
|
+
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
707
|
+
|
|
708
|
+
const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml' });
|
|
709
|
+
const url = window.URL.createObjectURL(blob)
|
|
710
|
+
const a = document.createElement('a')
|
|
711
|
+
a.setAttribute('href', url)
|
|
712
|
+
a.setAttribute('download', `${fileName}.svg`);
|
|
713
|
+
a.click()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Copies plot to clipboard as PNG image.
|
|
718
|
+
* @param {HTMLElement} btn - button element used to activate the copy event.
|
|
719
|
+
* @param {SVGAElement} svg - root SVG element of a plot.
|
|
720
|
+
* @param {number} width - width of the plot image to copy.
|
|
721
|
+
* @param {number} height - height of the plot image to copy.
|
|
722
|
+
*/
|
|
723
|
+
export function copyToClipboard(btn, svg, width, height) {
|
|
724
|
+
|
|
725
|
+
// save button conent
|
|
726
|
+
const content = btn.textContent;
|
|
727
|
+
|
|
728
|
+
navigator.clipboard.write([
|
|
729
|
+
new ClipboardItem({ 'image/png': createPngBlob(svg, width, height) })
|
|
730
|
+
])
|
|
731
|
+
.then(
|
|
732
|
+
() => {
|
|
733
|
+
// add class "copied" for visual confirmation
|
|
734
|
+
btn.classList.add('copied');
|
|
735
|
+
btn.textContent = '✓ done';
|
|
736
|
+
|
|
737
|
+
// Remove the class after 0.5 seconds and revert the text
|
|
738
|
+
setTimeout(() => {
|
|
739
|
+
btn.classList.remove('copied');
|
|
740
|
+
btn.textContent = content;
|
|
741
|
+
}, 500);
|
|
742
|
+
},
|
|
743
|
+
(e) => {
|
|
744
|
+
console.error(e)
|
|
745
|
+
|
|
746
|
+
// add class "error" for visual confirmation
|
|
747
|
+
btn.classList.add('error');
|
|
748
|
+
btn.textContent = 'x copy';
|
|
749
|
+
|
|
750
|
+
// Remove the class after 0.5 seconds and revert the text
|
|
751
|
+
setTimeout(() => {
|
|
752
|
+
btn.classList.remove('error');
|
|
753
|
+
btn.textContent = content;
|
|
754
|
+
}, 500);
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
.catch((error) => console.error(error));
|
|
758
|
+
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Returns plot BLOB to copy it to clipboard.
|
|
764
|
+
*
|
|
765
|
+
* @param {SVGElement} svg - root SVG element of a plot
|
|
766
|
+
* @param {number} width - desired image width in pixels
|
|
767
|
+
* @param {number} height - desired image height in pixels
|
|
768
|
+
*
|
|
769
|
+
*/
|
|
770
|
+
export async function createPngBlob(svg, width, height) {
|
|
771
|
+
|
|
772
|
+
// compute canvas size based on SVG size and scaling factor
|
|
773
|
+
// based on desired size in pixels
|
|
774
|
+
const svgHeight = svg.clientHeight;
|
|
775
|
+
const svgWidth = svg.clientWidth;
|
|
776
|
+
const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
|
|
777
|
+
|
|
778
|
+
// create a new canvas element and 2D context for it
|
|
779
|
+
const canvas = document.createElement('canvas');
|
|
780
|
+
const context = canvas.getContext('2d');
|
|
781
|
+
|
|
782
|
+
// set the canvas size to match the size of SVG element
|
|
783
|
+
canvas.width = svgWidth * scaleFactor;
|
|
784
|
+
canvas.height = svgHeight * scaleFactor;
|
|
785
|
+
|
|
786
|
+
// scale the context
|
|
787
|
+
context.scale(scaleFactor, scaleFactor)
|
|
788
|
+
|
|
789
|
+
// set corresponding attributes for SVG element
|
|
790
|
+
svg.setAttribute('width', canvas.width);
|
|
791
|
+
svg.setAttribute('height', canvas.height);
|
|
792
|
+
|
|
793
|
+
// create promise
|
|
794
|
+
const promise = new Promise((resolve, reject) => {
|
|
795
|
+
|
|
796
|
+
// serialzie SVG, create blob and URL for it
|
|
797
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
|
798
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
799
|
+
const DOMURL = window.URL || window.webkitURL || window;
|
|
800
|
+
const url = DOMURL.createObjectURL(svgBlob);
|
|
801
|
+
|
|
802
|
+
// create image an image and add callback
|
|
803
|
+
const img = new Image();
|
|
804
|
+
img.onload = function () {
|
|
805
|
+
|
|
806
|
+
// draw the image
|
|
807
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
808
|
+
DOMURL.revokeObjectURL(url);
|
|
809
|
+
|
|
810
|
+
// convert canvas to blob
|
|
811
|
+
canvas.toBlob(function (blob) {
|
|
812
|
+
if (blob) {
|
|
813
|
+
resolve(blob);
|
|
814
|
+
} else {
|
|
815
|
+
reject(new Error('Failed to create blob'));
|
|
816
|
+
}
|
|
817
|
+
}, 'image/png');
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// add error callback
|
|
821
|
+
img.onerror = function (err) {
|
|
822
|
+
reject(err);
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// assign value to run the onload callback
|
|
826
|
+
img.src = url;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// resolve promise and return the result
|
|
830
|
+
return (await promise);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Returns an array with 'n' colors.
|
|
836
|
+
*
|
|
837
|
+
* @param {number} n - number of colors to return.
|
|
838
|
+
* @param {string} alpha - string with hex number from 00 to FF defining alpha parameter for color.
|
|
839
|
+
*
|
|
840
|
+
* @returns {Array} array with colors as strings (hexadecimal).
|
|
841
|
+
*
|
|
842
|
+
*/
|
|
843
|
+
export function getcolmap(n, alpha) {
|
|
844
|
+
if (n > 16) n = 16;
|
|
845
|
+
const colmap = [
|
|
846
|
+
["#2679B2"],
|
|
847
|
+
["#2679B2", "#D22C2F"],
|
|
848
|
+
["#2679B2", "#92B42A", "#D22C2F"],
|
|
849
|
+
["#2679B2", "#2E9658", "#F2B825", "#D22C2F"],
|
|
850
|
+
["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
|
|
851
|
+
["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
|
|
852
|
+
["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
|
|
853
|
+
["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
|
|
854
|
+
["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
|
|
855
|
+
["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
|
|
856
|
+
["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
|
|
857
|
+
["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
|
|
858
|
+
["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
|
|
859
|
+
["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
|
|
860
|
+
["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
|
|
861
|
+
["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
return alpha ? colmap[n - 1].map(v => v + alpha) : colmap[n - 1];
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Computes coordinates and other parameters of main elements of x-axis
|
|
870
|
+
*
|
|
871
|
+
* @param {Array} limX - limits for x-axis.
|
|
872
|
+
* @param {Array} limY - limits for y-axis.
|
|
873
|
+
* @param {Array} scales - array of scale names (plot, x, y).
|
|
874
|
+
* @param {Array} tY - transposition matrix for y-coordinates.
|
|
875
|
+
* @param {Object} axis - JSON with x-axis parameters provided by user.
|
|
876
|
+
*
|
|
877
|
+
* @returns {Object} - JSON with coordinate of axis line, ticks, tick labels and factor.
|
|
878
|
+
*/
|
|
879
|
+
export function getXAxisParams(limX, limY, scales, tY, axis) {
|
|
880
|
+
|
|
881
|
+
let ticks = axis.ticks;
|
|
882
|
+
let tickLabels = axis.tickLabels;
|
|
883
|
+
let tickFactor = 0;
|
|
884
|
+
|
|
885
|
+
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
886
|
+
const ticksX = getAxisTicks(ticks, limX, XTICK_NUM[scales.x], true, axis.whole);
|
|
887
|
+
const tickNum = ticksX.length;
|
|
888
|
+
|
|
889
|
+
// compute tick y-coordinates (up and bottom)
|
|
890
|
+
const dY = invTransformObjects([TICK_SIZE[scales.plot]], tY)[0];
|
|
891
|
+
const ticksY1 = Vector.fill(limY[0], tickNum);
|
|
892
|
+
const ticksY2 = ticksY1.add(dY);
|
|
893
|
+
|
|
894
|
+
// compute coordinates for the ends of grid
|
|
895
|
+
const gridYEnd = Vector.fill(limY[1], tickNum);
|
|
896
|
+
|
|
897
|
+
// tick labels and tick factor
|
|
898
|
+
if (!tickLabels) {
|
|
899
|
+
if (ticks) {
|
|
900
|
+
tickFactor = 0;
|
|
901
|
+
tickLabels = ticks.v.map(v => trimNum(v).toString());
|
|
902
|
+
} else {
|
|
903
|
+
[tickFactor, tickLabels] = getTickLabels(ticksX.v);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
if (!tickLabels || tickLabels.length !== ticksX.length) {
|
|
909
|
+
console.error('XAxis: "tickLabels" must be a array of the same size as ticks.');
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// combine all coordinates together
|
|
914
|
+
const grid = [
|
|
915
|
+
[ticksX, ticksY1],
|
|
916
|
+
[ticksX, gridYEnd]
|
|
917
|
+
];
|
|
918
|
+
|
|
919
|
+
const axisLine = [
|
|
920
|
+
[vector([limX[0]]), vector([limY[0]])],
|
|
921
|
+
[vector([limX[1]]), vector([limY[0]])]
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
const tickCoords = [
|
|
925
|
+
[ticksX, ticksY2],
|
|
926
|
+
[ticksX, ticksY1]
|
|
927
|
+
];
|
|
928
|
+
|
|
929
|
+
const dX = (limX[1] - limX[0]) * 0.01;
|
|
930
|
+
const tfCoords = [
|
|
931
|
+
[null, null],
|
|
932
|
+
[[limX[1] - (tickFactor > 9 ? 1 : 0) * dX], [ticksY1.v[tickNum - 1]]],
|
|
933
|
+
];
|
|
934
|
+
|
|
935
|
+
const tfLabel = getTickFactorLabel(tickFactor);
|
|
936
|
+
|
|
937
|
+
// hide last label if it is too close to tick factor
|
|
938
|
+
const dtl = Math.abs(limX[1] - ticksX.v[ticksX.length - 1]);
|
|
939
|
+
const dl = limX[1] - limX[0];
|
|
940
|
+
const dt = Math.abs(ticksX.v[1] - ticksX.v[0]);
|
|
941
|
+
if (tfLabel.length > 0 && dtl < dl * 0.10 && dtl < dt * 0.5) {
|
|
942
|
+
tickLabels[tickLabels.length - 1] = ' ';
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return {grid, axisLine, tickCoords, tfCoords, ticks, tickLabels, tickFactor, tfLabel};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Computes coordinates and other parameters of main elements of y-axis
|
|
951
|
+
*
|
|
952
|
+
* @param {Array} limX - limits for x-axis.
|
|
953
|
+
* @param {Array} limY - limits for y-axis.
|
|
954
|
+
* @param {Array} scales - array of scale names (plot, x, y).
|
|
955
|
+
* @param {Array} tX - transposition matrix for x-coordinates.
|
|
956
|
+
* @param {Object} axis - JSON with x-axis parameters provided by user.
|
|
957
|
+
*
|
|
958
|
+
* @returns {Object} - JSON with coordinate of axis line, ticks, tick labels and factor.
|
|
959
|
+
*/
|
|
960
|
+
export function getYAxisParams(limX, limY, scales, tX, axis) {
|
|
961
|
+
|
|
962
|
+
let ticks = axis.ticks;
|
|
963
|
+
let tickLabels = axis.tickLabels;
|
|
964
|
+
let tickFactor = 0;
|
|
965
|
+
|
|
966
|
+
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
967
|
+
const ticksY = getAxisTicks(axis.ticks, limY, YTICK_NUM[scales.y], true, axis.whole);
|
|
968
|
+
const tickNum = ticksY.length;
|
|
969
|
+
|
|
970
|
+
// compute tick y-coordinates (up and bottom)
|
|
971
|
+
const dX = invTransformObjects([TICK_SIZE[scales.plot]], tX)[0];
|
|
972
|
+
const ticksX1 = Vector.fill(limX[0], tickNum)
|
|
973
|
+
const ticksX2 = ticksX1.add(dX)
|
|
974
|
+
|
|
975
|
+
// coordinates for the ends of grid
|
|
976
|
+
const gridXEnd = Vector.fill(limX[1], tickNum);
|
|
977
|
+
|
|
978
|
+
// tick labels and tick factor
|
|
979
|
+
if (!tickLabels) {
|
|
980
|
+
if (ticks) {
|
|
981
|
+
tickFactor = 0;
|
|
982
|
+
tickLabels = ticks.v.map(v => trimNum(v).toString());
|
|
983
|
+
} else {
|
|
984
|
+
[tickFactor, tickLabels] = getTickLabels(ticksY.v);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (tickLabels.length !== ticksY.length) {
|
|
989
|
+
console.error('YAxis: "tickLabels" must be a array of the same size as ticks.');
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// combine all coordinates together
|
|
994
|
+
const grid = [
|
|
995
|
+
[ticksX1, ticksY],
|
|
996
|
+
[gridXEnd, ticksY]
|
|
997
|
+
];
|
|
998
|
+
|
|
999
|
+
const axisLine = [
|
|
1000
|
+
[vector([limX[0]]), vector([limY[0]])],
|
|
1001
|
+
[vector([limX[0]]), vector([limY[1]])]
|
|
1002
|
+
]
|
|
1003
|
+
|
|
1004
|
+
const tickCoords = [
|
|
1005
|
+
[ticksX1, ticksY],
|
|
1006
|
+
[ticksX2, ticksY]
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
const tfCoords = [
|
|
1010
|
+
[[], []],
|
|
1011
|
+
[[ticksX2.v[tickNum - 1] + (Math.abs(tickFactor) > 9 ? 3 : 2) * dX], [limY[1]]],
|
|
1012
|
+
];
|
|
1013
|
+
|
|
1014
|
+
const tfLabel = getTickFactorLabel(tickFactor);
|
|
1015
|
+
|
|
1016
|
+
// hide last label if it is too close to tick factor
|
|
1017
|
+
if (tfLabel.length > 0 && (limY[1] - ticksY.v[ticksY.length - 1]) < ((limY[1] - limY[0]) * 0.05)) {
|
|
1018
|
+
tickLabels[tickLabels.length - 1] = ' ';
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return {grid, axisLine, tickCoords, tfCoords, ticks, tickLabels, tickFactor, tfLabel};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Performs sanity check of user defined parameters for colormap legend defines a couple of extra parameters.
|
|
1027
|
+
*
|
|
1028
|
+
* @param {Object} cml - JSON with user defined parameters from `ColormapLegend` component
|
|
1029
|
+
*
|
|
1030
|
+
* @returns {Object} objects with full set of parameters.
|
|
1031
|
+
*/
|
|
1032
|
+
export function getColormapLegendParams(cml) {
|
|
1033
|
+
|
|
1034
|
+
if (!cml.breaks) {
|
|
1035
|
+
console.error('ColormapLegend: breaks values must be provided.')
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const breaks = isvector(cml.breaks) ? cml.breaks.v : cml.breaks;
|
|
1040
|
+
|
|
1041
|
+
if (breaks.length < 2) {
|
|
1042
|
+
console.error('ColormapLegend: incorrect breaks values.')
|
|
1043
|
+
return null;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
if (!cml.colmap) {
|
|
1047
|
+
console.error('ColormapLegend: values for "colmap" parameter must be provided.');
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!Array.isArray(cml.colmap)) {
|
|
1052
|
+
console.error('ColormapLegend: parameter "colmap" must be array with colors.');
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (cml.colmap.length !== breaks.length - 1) {
|
|
1057
|
+
console.error('ColormapLegend: number of color values in "colmap" does not match number of intervals defined by breaks.');
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const colmap = cml.colmap;
|
|
1062
|
+
|
|
1063
|
+
let labels, labelsFactor;
|
|
1064
|
+
if (!cml.labels) {
|
|
1065
|
+
[labelsFactor, labels] = getTickLabels(breaks);
|
|
1066
|
+
} else {
|
|
1067
|
+
labels = cml.labels;
|
|
1068
|
+
labelsFactor = 0;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (!(labels.length === (breaks.length - 1) || labels.length === breaks.length)) {
|
|
1072
|
+
console.error('ColormapLegend: number of values in "labels" should be equal to or by one less than number of values in "breaks".');
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const isCentered = labels.length === colmap.length;
|
|
1077
|
+
return {colmap, breaks, labels, isCentered, labelsFactor, fontSize: cml.fontSize, labelColor: cml.labelColor};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Computes screen coordinates of colormap legend elements.
|
|
1083
|
+
*
|
|
1084
|
+
* @param {Object} cmlParams - JSON with colormap legend parameters from `getgColormapLegendParams`.
|
|
1085
|
+
* @param {number} axesWidth - width of axes in pixels.
|
|
1086
|
+
* @param {Array} pxMargins - array with axes margins in pixels.
|
|
1087
|
+
* @param {Object} scales - JSON with scales.
|
|
1088
|
+
*
|
|
1089
|
+
* @returns {Object} JSOn with coordinates.
|
|
1090
|
+
*/
|
|
1091
|
+
export function getColormapLegendCoords(cmlParams, axesWidth, pxMargins, scales) {
|
|
1092
|
+
|
|
1093
|
+
const n = cmlParams.colmap.length;
|
|
1094
|
+
const colmapWidth = axesWidth * COLMAP_WIDTH_RATIO;
|
|
1095
|
+
|
|
1096
|
+
// compute elements size and position
|
|
1097
|
+
const width = colmapWidth / n;
|
|
1098
|
+
const start = pxMargins[1] + (axesWidth - colmapWidth) * 0.5;
|
|
1099
|
+
const height = COLMAP_EL_HEIGHTS_PX[scales.y]
|
|
1100
|
+
const top = pxMargins[2] + height * COLMAP_TOP_MARGIN;
|
|
1101
|
+
const ltop = top + height * (1 + COLMAP_TOP_MARGIN)
|
|
1102
|
+
const dxl = cmlParams.isCentered ? width / 2 : 0
|
|
1103
|
+
|
|
1104
|
+
return {start, top, height, width, dxl, ltop};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Mouse click handler for plot elements.
|
|
1110
|
+
* @param {Object} e - event object.
|
|
1111
|
+
* @param {string} tagName - name of the tag the click is expected from.
|
|
1112
|
+
* @param {function} onclickCallback - callback function to run when click event happened.
|
|
1113
|
+
*/
|
|
1114
|
+
export function handleClick(e, tagName, onclickCallback) {
|
|
1115
|
+
if (!e || !tagName || !onclickCallback || typeof onclickCallback !== 'function') return;
|
|
1116
|
+
const el = e.target;
|
|
1117
|
+
if (el.tagName === tagName) {
|
|
1118
|
+
const id = Array.prototype.indexOf.call(el.parentNode.children, el);
|
|
1119
|
+
if (id < 0) return;
|
|
1120
|
+
onclickCallback(id);
|
|
1121
|
+
e.stopPropagation();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Computes width of text element for given font.
|
|
1128
|
+
*
|
|
1129
|
+
* @param {string} text - string with text
|
|
1130
|
+
* @param {string} font - font name and size, e.g. '12px Arial'
|
|
1131
|
+
*
|
|
1132
|
+
* @return {number} size of element in pixels
|
|
1133
|
+
*/
|
|
1134
|
+
export function getTextWidth(text, font) {
|
|
1135
|
+
const element = document.createElement('canvas');
|
|
1136
|
+
const context = element.getContext('2d');
|
|
1137
|
+
context.font = font;
|
|
1138
|
+
return context.measureText(text.replace(/<(.|\n)*?>/g, '')).width;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Compute size of main legend elements.
|
|
1144
|
+
*
|
|
1145
|
+
* @param {Array} items - array of legend items and their properties.
|
|
1146
|
+
* @param {number} fontSize - font size of a legend element in pixels.
|
|
1147
|
+
* @param {string} position - position of the legend.
|
|
1148
|
+
*
|
|
1149
|
+
* @return {Array} array with height and width of graphical part of legend item, padding size,
|
|
1150
|
+
* height and width of text labels (all in pixels).
|
|
1151
|
+
*/
|
|
1152
|
+
function getLegendSize(items, fontSize) {
|
|
1153
|
+
|
|
1154
|
+
// TODO: implement dynamic font family
|
|
1155
|
+
const fontName = 'Arial';
|
|
1156
|
+
|
|
1157
|
+
// compute size of graphical elements
|
|
1158
|
+
const elp = fontSize / 8; // top and bottom padding
|
|
1159
|
+
const elw = fontSize * 1.5; // width of legend element without label and padding
|
|
1160
|
+
|
|
1161
|
+
// compute size of text label elements
|
|
1162
|
+
// TODO: implement one row legend if position is "top" or "bottom"
|
|
1163
|
+
const elh = Array(items.length);
|
|
1164
|
+
const ely = Array(items.length);
|
|
1165
|
+
let y = 0;
|
|
1166
|
+
let lgh = 0;
|
|
1167
|
+
let lbw = 0;
|
|
1168
|
+
|
|
1169
|
+
for (let i = 0; i < items.length; i++) {
|
|
1170
|
+
const w = getTextWidth(' ' + items[i].label + ' ', fontSize + 'px ' + fontName);
|
|
1171
|
+
lbw = w > lbw ? w : lbw;
|
|
1172
|
+
const h = fontSize * items[i].labelHeight + 2 * elp;
|
|
1173
|
+
elh[i] = h;
|
|
1174
|
+
ely[i] = y;
|
|
1175
|
+
y = y + h;
|
|
1176
|
+
lgh = lgh + h;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const lgw = (elw + 2 * elp + lbw);
|
|
1180
|
+
return [ely, elh, elw, elp, lgh, lgw];
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Computes screen coordinates and sizes of legend and its elements.
|
|
1186
|
+
* @param {Object} params - object with legend params (items array and position name).
|
|
1187
|
+
* @param {number} fontSize - font size in puxels.
|
|
1188
|
+
* @param {Array} cpx - screen coordinates of axes corners along x.
|
|
1189
|
+
* @param {Array} cpy - screen coordinates of axes corners along y.
|
|
1190
|
+
* @param {number} ts - ticks size in pixels.
|
|
1191
|
+
*/
|
|
1192
|
+
export function getGroupLegendCoords(params, cpx, cpy, fontSize, ts) {
|
|
1193
|
+
|
|
1194
|
+
// compute size of legend elements
|
|
1195
|
+
const [ely, elh, elw, elp, lgh, lgw] = getLegendSize(params.items, fontSize * params.fontSize);
|
|
1196
|
+
|
|
1197
|
+
// compute coordinates of top left corner of the legend box
|
|
1198
|
+
const lgl = params.position.includes("left") ? cpx[0] + ts :
|
|
1199
|
+
params.position.includes("right") ? cpx[1] - lgw - ts :
|
|
1200
|
+
(cpx[1] + cpx[0] - lgw) * 0.5;
|
|
1201
|
+
|
|
1202
|
+
const lgt = params.position.includes("top") ? cpy[1] + ts :
|
|
1203
|
+
params.position.includes("bottom") ? cpy[0] - lgh - ts :
|
|
1204
|
+
(cpy[1] + cpy[0] - lgh) * 0.5;
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
return {lgl, lgt, lgw, lgh, ely, elh, elw, elp};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Converts text with super and subscripts to SVG text.
|
|
1213
|
+
* @param {string} text
|
|
1214
|
+
* @returns {string} converted text.
|
|
1215
|
+
*/
|
|
1216
|
+
export function text2svg(text) {
|
|
1217
|
+
|
|
1218
|
+
if (!text || text.length < 1) return text;
|
|
1219
|
+
|
|
1220
|
+
function toSuper(txt) {
|
|
1221
|
+
return '<tspan font-size="0.75em" dy="-0.5em">' + txt + '</tspan><tspan dy="0.4em"> </tspan>';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function toSub(txt) {
|
|
1225
|
+
return '<tspan font-size="0.75em" dy="0.5em">' + txt + '</tspan><tspan dy="-0.4em"> </tspan>';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
let i = 0;
|
|
1229
|
+
const dy = 0.5;
|
|
1230
|
+
const fs = 0.8;
|
|
1231
|
+
let svgText = '';
|
|
1232
|
+
while (i < text.length) {
|
|
1233
|
+
|
|
1234
|
+
const char = text[i];
|
|
1235
|
+
if ((char === '^' || char === '_') && i < text.length - 1) {
|
|
1236
|
+
|
|
1237
|
+
const isSuperscript = char === '^';
|
|
1238
|
+
i++;
|
|
1239
|
+
|
|
1240
|
+
if (text[i] === '(') {
|
|
1241
|
+
// multiscript, everything betwee ( and ) should be taken
|
|
1242
|
+
let j = i + 1;
|
|
1243
|
+
while (j < text.length && text[j] !== ')') j++;
|
|
1244
|
+
const scriptContent = text.slice(i + 1, j);
|
|
1245
|
+
svgText += isSuperscript ? toSuper(scriptContent) : toSub(scriptContent);
|
|
1246
|
+
i = j + 1; // Move past the closing '}'
|
|
1247
|
+
} else if (text[i] === '-' || text[i] === '–') {
|
|
1248
|
+
// negative script, take the sign and next element
|
|
1249
|
+
const scriptContent = text.slice(i, i + 2);
|
|
1250
|
+
svgText += isSuperscript ? toSuper(scriptContent) : toSub(scriptContent);
|
|
1251
|
+
i = i + 2;
|
|
1252
|
+
} else {
|
|
1253
|
+
svgText += isSuperscript ? toSuper(text[i]) : toSub(text[i]);
|
|
1254
|
+
i++;
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
svgText += char;
|
|
1258
|
+
i++;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return svgText;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Transforms 3D world coordinates to 2D scene pixels by applying the transformation matrix 'tM'.
|
|
1268
|
+
*
|
|
1269
|
+
* @param {Matrix} coords - matrix with coordinates [X, Y, Z]
|
|
1270
|
+
* @param {Matrix} tM - transformation matrix
|
|
1271
|
+
*
|
|
1272
|
+
* @returns {Array} array with transformed coordinates as two vectors [x, y].
|
|
1273
|
+
*
|
|
1274
|
+
*/
|
|
1275
|
+
export function transform3D(coords, tM) {
|
|
1276
|
+
const coords2D = cbind(coords, Vector.ones(coords.nrows)).dot(tM);
|
|
1277
|
+
return {x: coords2D.getcolref(1).map(v => roundCoords(v)), y: coords2D.getcolref(2).map(v => roundCoords(v))};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Compute coordinates of "latent" ticks by replicating lim[0] value and adding shifts.
|
|
1283
|
+
* @param {Array} lim - array with axis limits.
|
|
1284
|
+
* @param {number} tickNum - number of ticks to generate.
|
|
1285
|
+
* @returns
|
|
1286
|
+
*/
|
|
1287
|
+
function getLatentTicks(lim, tickNum) {
|
|
1288
|
+
const dl = (lim[1] - lim[0]) / 100; // 1% of axis size
|
|
1289
|
+
const ticks = Vector.fill(lim[0], tickNum);
|
|
1290
|
+
const ticks1 = ticks.subtract(dl * 1.5);
|
|
1291
|
+
const ticks2 = ticks.add(dl * 1.5);
|
|
1292
|
+
const ticks3 = ticks.subtract(dl * 5);
|
|
1293
|
+
|
|
1294
|
+
return [ticks, ticks1, ticks2, ticks3];
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Computes coordinates of all elements (lines, labels, etc) for x-axis ticks.
|
|
1300
|
+
* @param {Object} xaxis - JSON with x-axis paramaters.
|
|
1301
|
+
* @param {Array} limX - array with x-axis limits.
|
|
1302
|
+
* @param {Array} limY - array with y-axis limits.
|
|
1303
|
+
* @param {Array} limZ - array with z-axis limits.
|
|
1304
|
+
* @param {string} scale - current scale value.
|
|
1305
|
+
* @returns {Object} JSON with coordinates.
|
|
1306
|
+
*/
|
|
1307
|
+
export function getXAxisCoords3D(xaxis, limX, limY, limZ, scale) {
|
|
1308
|
+
|
|
1309
|
+
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1310
|
+
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1311
|
+
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1312
|
+
const ticksX = getAxisTicks(xaxis.ticks, limX, TICK_NUM[scale], true);
|
|
1313
|
+
const tickNum = ticksX.length;
|
|
1314
|
+
|
|
1315
|
+
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|
|
1316
|
+
const [ticksZ, ticksZ1, ticksZ2, ticksZ3] = getLatentTicks(limZ, tickNum);
|
|
1317
|
+
|
|
1318
|
+
// coordinates for the ends of grid
|
|
1319
|
+
const gridYEnd = Vector.fill(limY[1], tickNum);
|
|
1320
|
+
const gridZEnd = Vector.fill(limZ[1], tickNum);
|
|
1321
|
+
|
|
1322
|
+
// tick labels
|
|
1323
|
+
const tickLabels = (!xaxis.ticks || !xaxis.tickLabels) ? ticksX.v : xaxis.tickLabels;
|
|
1324
|
+
if (tickLabels.length !== ticksX.length) {
|
|
1325
|
+
console.error('XAxis (3D): "tickLabels" must be a array of the same size as ticks.');
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// combine all coordinates together
|
|
1329
|
+
const grid1 = [
|
|
1330
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1331
|
+
cbind(ticksX, gridYEnd, ticksZ)
|
|
1332
|
+
];
|
|
1333
|
+
|
|
1334
|
+
const grid2 = [
|
|
1335
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1336
|
+
cbind(ticksX, ticksY, gridZEnd)
|
|
1337
|
+
];
|
|
1338
|
+
|
|
1339
|
+
const axisLine = [
|
|
1340
|
+
matrix([limX[0], limY[0], limZ[0]], 1, 3),
|
|
1341
|
+
matrix([limX[1], limY[0], limZ[0]], 1, 3)
|
|
1342
|
+
]
|
|
1343
|
+
|
|
1344
|
+
const tickCoords = [
|
|
1345
|
+
cbind(ticksX, ticksY, ticksZ), // middle point
|
|
1346
|
+
cbind(ticksX, ticksY2, ticksZ), // middle point with positive shift along Y
|
|
1347
|
+
cbind(ticksX, ticksY, ticksZ2), // middle point with positive shift along Z
|
|
1348
|
+
cbind(ticksX, ticksY1, ticksZ1), // middle point with negative shift along Y and Z
|
|
1349
|
+
cbind(ticksX, ticksY3, ticksZ3) // middle point with negative shift along Y and Z for ticks
|
|
1350
|
+
];
|
|
1351
|
+
|
|
1352
|
+
// here we do not need to make a matrix as the three values will be used as vectors
|
|
1353
|
+
const titleCoords = [
|
|
1354
|
+
[limX[1] + 3 * dX], [limY[0] - 2 * dY], [limZ[0] - 2 * dZ]
|
|
1355
|
+
];
|
|
1356
|
+
|
|
1357
|
+
return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Computes coordinates of all elements (lines, labels, etc) for y-axis ticks.
|
|
1363
|
+
* @param {Object} yaxis - JSON with y-axis paramaters.
|
|
1364
|
+
* @param {Array} limX - array with x-axis limits.
|
|
1365
|
+
* @param {Array} limY - array with y-axis limits.
|
|
1366
|
+
* @param {Array} limZ - array with z-axis limits.
|
|
1367
|
+
* @param {string} scale - current scale value.
|
|
1368
|
+
* @returns {Object} JSON with coordinates.
|
|
1369
|
+
*/
|
|
1370
|
+
export function getYAxisCoords3D(yaxis, limX, limY, limZ, scale) {
|
|
1371
|
+
|
|
1372
|
+
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1373
|
+
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1374
|
+
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1375
|
+
const ticksY = getAxisTicks(yaxis.ticks, limY, TICK_NUM[scale], true);
|
|
1376
|
+
const tickNum = ticksY.length;
|
|
1377
|
+
|
|
1378
|
+
const [ticksX, ticksX1, ticksX2, ticksX3] = getLatentTicks(limX, tickNum);
|
|
1379
|
+
const [ticksZ, ticksZ1, ticksZ2, ticksZ3] = getLatentTicks(limZ, tickNum);
|
|
1380
|
+
|
|
1381
|
+
// coordinates for the ends of grid
|
|
1382
|
+
const gridXEnd = Vector.fill(limX[1], tickNum);
|
|
1383
|
+
const gridZEnd = Vector.fill(limZ[1], tickNum);
|
|
1384
|
+
|
|
1385
|
+
// tick labels
|
|
1386
|
+
const tickLabels = (!yaxis.ticks || !yaxis.tickLabels) ? ticksY.v : yaxis.tickLabels;
|
|
1387
|
+
if (tickLabels.length !== ticksY.length) {
|
|
1388
|
+
console.error('YAxis (3D): "tickLabels" must be a array of the same size as ticks.');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// combine all coordinates together
|
|
1392
|
+
const grid1 = [
|
|
1393
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1394
|
+
cbind(gridXEnd, ticksY, ticksZ)
|
|
1395
|
+
];
|
|
1396
|
+
|
|
1397
|
+
const grid2 = [
|
|
1398
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1399
|
+
cbind(ticksX, ticksY, gridZEnd)
|
|
1400
|
+
];
|
|
1401
|
+
|
|
1402
|
+
const axisLine = [
|
|
1403
|
+
matrix([limX[0], limY[0], limZ[0]], 1, 3),
|
|
1404
|
+
matrix([limX[0], limY[1], limZ[0]], 1, 3)
|
|
1405
|
+
]
|
|
1406
|
+
|
|
1407
|
+
const tickCoords = [
|
|
1408
|
+
cbind(ticksX, ticksY, ticksZ), // middle point
|
|
1409
|
+
cbind(ticksX2, ticksY, ticksZ), // middle point with positive shift along X
|
|
1410
|
+
cbind(ticksX, ticksY, ticksZ2), // middle point with positive shift along Z
|
|
1411
|
+
cbind(ticksX1, ticksY, ticksZ1), // middle point with negative shift along X and Z
|
|
1412
|
+
cbind(ticksX3, ticksY, ticksZ3) // middle point with negative shift along X and Z for ticks
|
|
1413
|
+
];
|
|
1414
|
+
|
|
1415
|
+
// here we do not need to make a matrix as the three values will be used as vectors
|
|
1416
|
+
const titleCoords = [
|
|
1417
|
+
[limX[0] - 2 * dX], [limY[1] + 3 * dY], [limZ[0] - 2 * dZ]
|
|
1418
|
+
];
|
|
1419
|
+
|
|
1420
|
+
return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Computes coordinates of all elements (lines, labels, etc) for z-axis ticks.
|
|
1426
|
+
* @param {Object} yaxis - JSON with z-axis paramaters.
|
|
1427
|
+
* @param {Array} limX - array with x-axis limits.
|
|
1428
|
+
* @param {Array} limY - array with y-axis limits.
|
|
1429
|
+
* @param {Array} limZ - array with z-axis limits.
|
|
1430
|
+
* @param {string} scale - current scale value.
|
|
1431
|
+
* @returns {Object} JSON with coordinates.
|
|
1432
|
+
*/
|
|
1433
|
+
export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
1434
|
+
|
|
1435
|
+
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1436
|
+
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1437
|
+
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1438
|
+
const ticksZ = getAxisTicks(zaxis.ticks, limZ, TICK_NUM[scale], true);
|
|
1439
|
+
const tickNum = ticksZ.length;
|
|
1440
|
+
|
|
1441
|
+
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|
|
1442
|
+
const [ticksX, ticksX1, ticksX2, ticksX3] = getLatentTicks(limX, tickNum);
|
|
1443
|
+
|
|
1444
|
+
// coordinates for the ends of grid
|
|
1445
|
+
const gridYEnd = Vector.fill(limY[1], tickNum);
|
|
1446
|
+
const gridXEnd = Vector.fill(limX[1], tickNum);
|
|
1447
|
+
|
|
1448
|
+
// tick labels
|
|
1449
|
+
const tickLabels = (!zaxis.ticks || !zaxis.tickLabels) ? ticksZ.v : zaxis.tickLabels;
|
|
1450
|
+
if (tickLabels.length !== ticksX.length) {
|
|
1451
|
+
console.error('XAxis (3D): "tickLabels" must be a array of the same size as ticks.');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// combine all coordinates together
|
|
1455
|
+
const grid1 = [
|
|
1456
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1457
|
+
cbind(ticksX, gridYEnd, ticksZ)
|
|
1458
|
+
];
|
|
1459
|
+
|
|
1460
|
+
const grid2 = [
|
|
1461
|
+
cbind(ticksX, ticksY, ticksZ),
|
|
1462
|
+
cbind(gridXEnd, ticksY, ticksZ)
|
|
1463
|
+
];
|
|
1464
|
+
|
|
1465
|
+
const axisLine = [
|
|
1466
|
+
matrix([limX[0], limY[0], limZ[0]], 1, 3),
|
|
1467
|
+
matrix([limX[0], limY[0], limZ[1]], 1, 3)
|
|
1468
|
+
]
|
|
1469
|
+
|
|
1470
|
+
const tickCoords = [
|
|
1471
|
+
cbind(ticksX, ticksY, ticksZ), // middle point
|
|
1472
|
+
cbind(ticksX, ticksY2, ticksZ), // middle point with positive shift along Y
|
|
1473
|
+
cbind(ticksX2, ticksY, ticksZ), // middle point with positive shift along X
|
|
1474
|
+
cbind(ticksX1, ticksY1, ticksZ), // middle point with negative shift along Y and X
|
|
1475
|
+
cbind(ticksX3, ticksY3, ticksZ) // middle point with negative shift along Y and X for ticks
|
|
1476
|
+
];
|
|
1477
|
+
|
|
1478
|
+
// here we do not need to make a matrix as the three values will be used as vectors
|
|
1479
|
+
const titleCoords = [
|
|
1480
|
+
[limX[0] - 2 * dX], [limY[0] - 2 * dY], [limZ[1] + 3 * dZ]
|
|
1481
|
+
];
|
|
1482
|
+
|
|
1483
|
+
return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
|
|
1484
|
+
}
|