svelte-plots-basic 2.5.8 → 3.0.1

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/src/Utils.js DELETED
@@ -1,639 +0,0 @@
1
- import { cbind, vector, isvector, Vector } from 'mdatools/arrays';
2
- import { min, max, diff } from 'mdatools/stat';
3
-
4
-
5
- /**
6
- * Creates label for a tick factor.
7
- *
8
- * @param {Number} tf - tick factor value (negative or positive)
9
- * @returns
10
- */
11
- export function getTickFactorLabel(tf) {
12
- if (tf === 0) return '';
13
- return `<tspan>&times;10</tspan><tspan baseline-shift="super">${(tf).toString()}</tspan>`;
14
- }
15
-
16
-
17
- /**
18
- * Trim number leaving only significant decimals.
19
- * @param {number} num
20
- * @returns trimmed number
21
- */
22
- export function trimNum(num) {
23
-
24
- // check if number is whole — return as is
25
- if (num === Math.round(num)) return num;
26
-
27
- // Convert to string with precision to maintain significant digits
28
- let str = num.toPrecision(12);
29
-
30
- // Remove trailing zeros
31
- str = str.replace(/\.?0+$/, '');
32
-
33
- // Convert back to number
34
- return parseFloat(str);
35
- }
36
-
37
- /**
38
- * Generate vector with axis tick labels and tick factor based on numeric tick values.
39
- *
40
- * @param {Vector} ticks - vector with numeric tick values.
41
- *
42
- * @returns {Array} an array with tick factor and array with tick labels.
43
- */
44
- export function getTickLabels(ticks) {
45
-
46
- if (ticks === undefined) return undefined;
47
- if (ticks.length === 1) return [0, [ticks[0].toString()]];
48
-
49
- // compute and correct step between ticks
50
- let step = trimNum(Math.abs(min(diff(ticks))));
51
- let tickFactor = 0;
52
-
53
- // step is large (over 1)
54
- if (step >= 1 && ticks.length > 1) {
55
- if (step < 100) return [0, Array.from(ticks).map(v => v.toFixed(0))];
56
- let digNum = Math.ceil(trimNum(Math.log10(step)));
57
- tickFactor = digNum - 2;
58
- return [tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toFixed(0))];
59
- }
60
-
61
- let decNum = Math.ceil(-trimNum(Math.log10(step)));
62
- if (decNum <= 2) {
63
- return [0, Array.from(ticks).map(v => v.toFixed(decNum))];
64
- }
65
- tickFactor = decNum - 2;
66
- decNum = 2;
67
-
68
- if (trimNum(Math.abs(max(ticks))) <= 10) {
69
- return [-tickFactor, Array.from(ticks).map(v => (v * Math.pow(10, tickFactor)).toFixed(decNum))];
70
- } else {
71
- return [-tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toString())];
72
- }
73
- }
74
-
75
-
76
- /**
77
- * Check coordinates and convert them to vector if necessary.
78
- *
79
- * @param {Array|Vector} x - vector or array with coordinates.
80
- * @param {string} source - name of component which called the method (needed for error message).
81
- *
82
- * @returns {Vector}
83
- *
84
- */
85
- export function checkCoords(x, source) {
86
-
87
- if (Array.isArray(x)) {
88
- x = vector(x);
89
- }
90
-
91
- if (!isvector(x)) {
92
- throw Error(source + ": coordinates must be provided as array or instance of Vector class.");
93
- }
94
-
95
- if (x.length < 1) {
96
- throw Error(source + ": vector with coordinates is empty.");
97
- }
98
-
99
- return x;
100
- }
101
-
102
-
103
- /**
104
- * Create string with coordinates of SVG polygon in 3D.
105
- *
106
- * @param {Array|Vector} x - vector with x-coordinates of polygon points.
107
- * @param {Array|Vector} y - vector with y-coordinates of polygon points.
108
- * @param {Array|Vector} z - vector with y-coordinates of polygon points.
109
- * @param {Array} tM - transformation matrix for 3D->2D (from 'Axes').
110
- * @param {Object} axes - JSON with Axes context.
111
- *
112
- * @returns {string} string with coordinates.
113
- *
114
- */
115
- export function val2p3d(x, y, z, tM, axes) {
116
-
117
- if (x === undefined || y === undefined) return undefined;
118
-
119
- const [px, py] = axes.transform(cbind(checkCoords(x), checkCoords(y), checkCoords(z)), tM);
120
-
121
- if (px.length !== py.length) {
122
- throw Error('PlotSeries: parameters "xValues", "yValues" and "zValues" must be numeric vectors of the same length.');
123
- }
124
-
125
- let p = "";
126
- for (let i = 0; i < px.length; i++) {
127
- p += px[i] + "," + py[i] + " ";
128
- }
129
-
130
- return p;
131
- }
132
-
133
-
134
- /**
135
- * Create string with coordinates of SVG polygon.
136
- *
137
- * @param {Array|Vector} x - vector with x-coordinates of polygon points.
138
- * @param {Array|Vector} y - vector with y-coordinates of polygon points.
139
- * @param {Array} tX - transformation parameters for x-coordinates (from 'Axes').
140
- * @param {Array} tY - transformation parameters for y-coordinates (from 'Axes').
141
- * @param {Object} axes - JSON with Axes context.
142
- *
143
- * @returns {string} string with coordinates.
144
- *
145
- */
146
- export function val2p(x, y, tX, tY, axes) {
147
-
148
- if (x === undefined || y === undefined) return undefined;
149
-
150
- const px = axes.transform(checkCoords(x, 'val2p'), tX.coords);
151
- const py = axes.transform(checkCoords(y, 'val2p'), tY.coords);
152
-
153
- if (px.length != py.length) {
154
- throw Error('PlotSeries: parameters "xValues" and "yValues" must be numeric vectors of the same length.');
155
- }
156
-
157
- let p = "";
158
- for (let i = 0; i < px.length; i++) {
159
- p += px[i] + "," + py[i] + " ";
160
- }
161
-
162
- return p;
163
- }
164
-
165
- /**
166
- * Computes nice tick values for axis.
167
- *
168
- * @param {Array} ticks - vector with ticks if alredy available (if not, new will be computed).
169
- * @param {Array} lim - vector with axis limits tickets must be computed for.
170
- * @param {number} maxTickNum - maximum number of ticks to compute.
171
- * @param {boolean} round - round or not the fractions when computing nice numbers for the ticks.
172
- * @param {boolean} whole - should the ticks be a whole number.
173
- * @param {Number} deltaFactor - percent of range to use as internal margins
174
- *
175
- * @returns {Array} an array with computed tick positions.
176
- *
177
- */
178
- export function getAxisTicks(ticks, lim, maxTickNum, round, whole, deltaFactor) {
179
-
180
- if (round === undefined || round === null) {
181
- round = true;
182
- }
183
-
184
- if (whole === undefined || whole === null) {
185
- whole = false;
186
- }
187
-
188
- if (deltaFactor === undefined) {
189
- deltaFactor = 0.001;
190
- }
191
-
192
- // if ticks are already provided do not recompute them
193
- if (ticks !== undefined) {
194
- if (Array.isArray(ticks)) {
195
- ticks = vector(ticks);
196
- }
197
-
198
- if (!isvector(ticks)) {
199
- throw Error('getAxisTicks: axis ticks must be provided as an array or as a vector.');
200
- }
201
-
202
- return ticks.filter(x => x >= lim[0] & x <= lim[1]);
203
- }
204
-
205
- // check if limits are ok
206
- if (typeof(lim) !== "object" || lim[0] === undefined || lim[1] === undefined) return undefined;
207
-
208
- // get range as a nice number and compute min, max and steps for the tick sequence
209
- const delta = Math.abs((lim[1] - lim[0])) * deltaFactor;
210
- const localRange = lim[1] - lim[0] - 2 * delta;
211
- const exponent = Math.floor(Math.log10(localRange));
212
- const fraction = 1 / Math.pow(10, exponent - 1);
213
- const range = Math.round(localRange * fraction) / fraction;
214
-
215
- let tickSpacing = niceNum(range / maxTickNum, false);
216
- if (whole) {
217
- if (tickSpacing < 1) tickSpacing = 1;
218
- tickSpacing = Math.round(tickSpacing);
219
- }
220
-
221
- // compute smallest and largest tick rounded to tickSpacing
222
- let tickMin = Math.ceil((lim[0] + delta ) / tickSpacing) * tickSpacing;
223
- let tickMax = Math.floor((lim[1] - delta) / tickSpacing) * tickSpacing;
224
-
225
- // adjust largest tick if it is too close to axis limit and tick spacing
226
- // is small (so tick factor may appear)
227
- if (tickSpacing < 0.1 && (lim[1] - tickMax) < (tickSpacing / 2)) tickMax = tickMax - tickSpacing;
228
-
229
- // recompute maxTickNum
230
- maxTickNum = (Math.ceil(trimNum((tickMax - tickMin) / tickSpacing)) + 1);
231
-
232
- // create a sequence of ticks
233
- ticks = Vector.zeros(maxTickNum);
234
- for (let i = 0; i < maxTickNum; i++) {
235
- ticks.v[i] = tickMin + tickSpacing * i;
236
- }
237
-
238
- // if step is smaller than 1 round values to remove small decimals accidentially added by JS
239
- if (Math.abs(tickSpacing) < 1) {
240
- const decNum = Math.round(-Math.log10(tickSpacing));
241
- const r = Math.pow(10, 1 + Math.round(-Math.log10(tickSpacing)));
242
- ticks = ticks.apply(v => (Math.round((v + Number.EPSILON) * r) / r));
243
- }
244
-
245
- // make sure the ticks are not aligned with axes limits
246
- return ticks.filter(x => x >= lim[0] & x <= lim[1]);
247
- }
248
-
249
-
250
- /**
251
- * Computes a nice spacing value for a given range.
252
- *
253
- * @param {Number} localRange - a range (max - min).
254
- * @param {boolean} round - round or not the fractions when computing the number.
255
- *
256
- * @returns {Number} the computed spacing value.
257
- *
258
- */
259
- export function niceNum(localRange, round) {
260
-
261
- const exponent = Math.floor(Math.log10(localRange));
262
- const fraction = localRange / Math.pow(10, exponent);
263
- let niceFraction;
264
-
265
- if (round) {
266
- if (fraction < 1.5)
267
- niceFraction = 1;
268
- else if (fraction < 3)
269
- niceFraction = 2;
270
- else if (fraction < 7)
271
- niceFraction = 5;
272
- else
273
- niceFraction = 10;
274
- } else {
275
- if (fraction <= 1)
276
- niceFraction = 1;
277
- else if (fraction <= 2)
278
- niceFraction = 2;
279
- else if (fraction <= 5)
280
- niceFraction = 5;
281
- else
282
- niceFraction = 10;
283
- }
284
-
285
- return niceFraction * Math.pow(10, exponent);
286
- }
287
-
288
-
289
- /**
290
- * Rounds coordinate values to single decimal
291
- */
292
- export function roundCoords(x) {
293
- return Math.round(x * 10) / 10
294
- }
295
-
296
-
297
- /**
298
- * Computes a scale level.
299
- *
300
- * @param {numeric} width - width of plotting area in pixels.
301
- * @param {numeric} height - height of plotting area in pixels.
302
- *
303
- * @returns {text} the scale level ("small", "medium" or "large").
304
- *
305
- */
306
- export function getScale(width, height) {
307
- if (height < 300.2 || width < 400.2) return "small";
308
- if (height < 400.2 || width < 700.2) return "medium";
309
- if (height < 650.2 || width < 850.2) return "large";
310
- return "xlarge";
311
- }
312
-
313
- /**
314
- * Computes a scale level for single axis.
315
- *
316
- * @param {numeric} size - width or height of plotting area in pixels.
317
- * @returns {text} the scale level ("small", "medium" or "large").
318
- *
319
- */
320
- export function getAxisScale(width, size) {
321
- if (width < 400.2) return "small";
322
- if (width < 700.2) return "medium";
323
- if (width < 900.2) return "large";
324
- return "xlarge";
325
- }
326
-
327
- /*************************************************************/
328
- /* Functions needed to adjust DPI of resulting PNG image */
329
- /* taken from: https://github.com/shutterstock/changeDPI */
330
- /* and modified */
331
- /*************************************************************/
332
-
333
- /**
334
- * Create table for PNG CRC calculation
335
- */
336
- function createPngDataTable() {
337
- const crcTable = new Int32Array(256);
338
- for (let n = 0; n < 256; n++) {
339
- let c = n;
340
- for (let k = 0; k < 8; k++) {
341
- c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
342
- }
343
- crcTable[n] = c;
344
- }
345
- return crcTable;
346
- }
347
-
348
- /**
349
- * Calculate CRC for PNG image
350
- * @param buf image data
351
- */
352
- function calcCrc(buf) {
353
- let c = -1;
354
- let pngDataTable = createPngDataTable();
355
- for (let n = 0; n < buf.length; n++) {
356
- c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
357
- }
358
- return c ^ -1;
359
- }
360
-
361
- /**
362
- * Change DPI of PNG image
363
- * @param dataArray image data
364
- * @param dpi new DPI
365
- */
366
- function changeDpiOnArray(dataArray, dpi) {
367
-
368
- const physChunk = new Uint8Array(13);
369
- // chunk header pHYs
370
- // 9 bytes of data
371
- // 4 bytes of crc
372
- // this multiplication is because the standard is dpi per meter.
373
- dpi *= 39.3701;
374
- physChunk[0] = 'p'.charCodeAt(0);
375
- physChunk[1] = 'H'.charCodeAt(0);
376
- physChunk[2] = 'Y'.charCodeAt(0);
377
- physChunk[3] = 's'.charCodeAt(0);
378
- physChunk[4] = dpi >>> 24; // dpiX byte 3
379
- physChunk[5] = dpi >>> 16; // dpiX byte 2
380
- physChunk[6] = dpi >>> 8; // dpiX byte 1
381
- physChunk[7] = dpi & 0xff; // dpiX byte 0
382
- physChunk[8] = physChunk[4]; // dpiY byte 3
383
- physChunk[9] = physChunk[5]; // dpiY byte 2
384
- physChunk[10] = physChunk[6]; // dpiY byte 1
385
- physChunk[11] = physChunk[7]; // dpiY byte 0
386
- physChunk[12] = 1; // dot per meter....
387
-
388
- // compute and add CRC for physChunk
389
- const crc = calcCrc(physChunk);
390
- const crcChunk = new Uint8Array(4);
391
- crcChunk[0] = crc >>> 24;
392
- crcChunk[1] = crc >>> 16;
393
- crcChunk[2] = crc >>> 8;
394
- crcChunk[3] = crc & 0xff;
395
-
396
- // chunk structur 4 bytes for length is 9
397
- const chunkLength = new Uint8Array(4);
398
- chunkLength[0] = 0;
399
- chunkLength[1] = 0;
400
- chunkLength[2] = 0;
401
- chunkLength[3] = 9;
402
-
403
- const finalHeader = new Uint8Array(54);
404
- finalHeader.set(dataArray, 0);
405
- finalHeader.set(chunkLength, 33);
406
- finalHeader.set(physChunk, 37);
407
- finalHeader.set(crcChunk, 50);
408
- return finalHeader;
409
- }
410
-
411
- /**
412
- * Change DPI of PNG image
413
- * @param base64Image image data
414
- * @param dpi new DPI
415
- */
416
- function changeDPI(base64Image, dpi) {
417
-
418
- const dataSplitted = base64Image.split(',');
419
- const format = dataSplitted[0];
420
- const body = dataSplitted[1];
421
-
422
- // here we assume there is pHYS chunk in the header
423
- const headerLength = 33 / 3 * 4;
424
-
425
- const stringHeader = body.substring(0, headerLength);
426
- const restOfData = body.substring(headerLength);
427
- const headerBytes = atob(stringHeader);
428
- const dataArray = new Uint8Array(headerBytes.length);
429
- for (let i = 0; i < dataArray.length; i++) {
430
- dataArray[i] = headerBytes.charCodeAt(i);
431
- }
432
- const finalArray = changeDpiOnArray(dataArray, dpi, 'image/png');
433
- const base64Header = btoa(String.fromCharCode(...finalArray));
434
- return [format, ',', base64Header, restOfData].join('');
435
- }
436
-
437
-
438
- /**
439
- * Downloads plot (or any other SVG image) as a PNG file.
440
- *
441
- * @param {SVGElement} svg - root SVG element of a plot
442
- * @param {string} fileName - filename to save with (without extension)
443
- * @param {number} width - desired image width in cm
444
- * @param {number} height - desired image height in cm
445
- * @param {number} res - resolution (pixels per inch)
446
- *
447
- */
448
- export function downloadPNG (svg, fileName, width, height, res) {
449
-
450
- if (!width) {
451
- width = 10;
452
- }
453
-
454
- if (!height) {
455
- height = 10;
456
- }
457
-
458
- if (!res) {
459
- res = 300;
460
- }
461
-
462
- if (width < 1 || width > 30) {
463
- throw Error('Parameter "width" must be between 1 and 30 (cm).');
464
- }
465
-
466
- if (height < 1 || height > 30) {
467
- throw Error('Parameter "height" must be between 1 and 30 (cm).');
468
- }
469
-
470
- if (res < 50 || res > 1200) {
471
- throw Error('Parameter "res" must be between 50 and 1200 (ppi).');
472
- }
473
-
474
- if (!svg || !(svg instanceof SVGElement)) {
475
- throw Error('Parameter "svg" is not an instance of SVGElement.');
476
- }
477
-
478
- if (!fileName || fileName.trim() === '') {
479
- fileName = 'plot';
480
- }
481
-
482
- // recalculate width and height to pixels
483
- width = width / 2.54 * res;
484
- height = height / 2.54 * res;
485
-
486
- setTimeout(() => {
487
-
488
- // compute canvas size based on SVG size and desired PNG WIDTH in pixels
489
- const svgHeight = svg.clientHeight;
490
- const svgWidth = svg.clientWidth;
491
- const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
492
-
493
- // create a new canvas element
494
- const canvas = document.createElement('canvas');
495
- const context = canvas.getContext('2d');
496
-
497
- // set the canvas size to match the SVG element
498
- canvas.width = svgWidth * scaleFactor;
499
- canvas.height = svgHeight * scaleFactor;
500
-
501
- // set corresponding attributes for SVG element
502
- svg.setAttribute('width', canvas.width);
503
- svg.setAttribute('height', canvas.height);
504
-
505
- // draw a white background
506
- context.fillStyle = 'white';
507
- context.scale(scaleFactor, scaleFactor)
508
- context.fillRect(0, 0, canvas.width, canvas.height);
509
-
510
- // draw the SVG onto the canvas
511
- const svgXml = new XMLSerializer().serializeToString(svg);
512
- const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
513
- const url = URL.createObjectURL(blob);
514
-
515
- const img = new Image();
516
-
517
- img.onload = function () {
518
-
519
- // draw the image
520
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
521
-
522
- // create a data URL for the canvas and adjust its DPI
523
- const dataURL = canvas.toDataURL('image/png');
524
- const dataURL2 = changeDPI(dataURL, res);
525
-
526
- // create a temporary link and trigger the download
527
- const downloadLink = document.createElement('a');
528
- downloadLink.href = dataURL2;
529
- downloadLink.download = `${fileName}.png`;
530
- downloadLink.click();
531
- };
532
-
533
- // set the image source
534
- img.src = url;
535
- }, 20);
536
- }
537
-
538
-
539
- /**
540
- * Downloads plot (or any other SVG image) as an SVG file.
541
- *
542
- * @param {SVGElement} svg - root SVG element of a plot
543
- * @param {string} fileName - filename to save with (without extension)
544
- */
545
- export function downloadSVG(svg, fileName) {
546
-
547
- if (!svg || !(svg instanceof SVGElement)) {
548
- throw Error('Variable "svg" is not an instance of SVGElement.');
549
- }
550
-
551
- if (!fileName || fileName.trim() === "") {
552
- fileName = "plot";
553
- }
554
-
555
- const svgHeight = svg.clientHeight;
556
- const svgWidth = svg.clientWidth;
557
- svg.setAttribute('width', svgWidth + 'px');
558
- svg.setAttribute('height', svgHeight + 'px');
559
- svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
560
-
561
- const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml' });
562
- const url = window.URL.createObjectURL(blob)
563
- const a = document.createElement('a')
564
- a.setAttribute('href', url)
565
- a.setAttribute('download', `${fileName}.svg`);
566
- a.click()
567
- }
568
-
569
-
570
- /**
571
- * Returns plot BLOB to copy it to clipboard.
572
- *
573
- * @param {SVGElement} svg - root SVG element of a plot
574
- * @param {number} width - desired image width in pixels
575
- * @param {number} height - desired image height in pixels
576
- *
577
- */
578
- export async function createPngBlob(svg, width, height) {
579
-
580
- // compute canvas size based on SVG size and scaling factor
581
- // based on desired size in pixels
582
- const svgHeight = svg.clientHeight;
583
- const svgWidth = svg.clientWidth;
584
- const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
585
-
586
- // create a new canvas element and 2D context for it
587
- const canvas = document.createElement('canvas');
588
- const context = canvas.getContext('2d');
589
-
590
- // set the canvas size to match the size of SVG element
591
- canvas.width = svgWidth * scaleFactor;
592
- canvas.height = svgHeight * scaleFactor;
593
-
594
- // scale the context
595
- context.scale(scaleFactor, scaleFactor)
596
-
597
- // set corresponding attributes for SVG element
598
- svg.setAttribute('width', canvas.width);
599
- svg.setAttribute('height', canvas.height);
600
-
601
- // create promise
602
- const promise = new Promise((resolve, reject) => {
603
-
604
- // serialzie SVG, create blob and URL for it
605
- const svgData = new XMLSerializer().serializeToString(svg);
606
- const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
607
- const DOMURL = window.URL || window.webkitURL || window;
608
- const url = DOMURL.createObjectURL(svgBlob);
609
-
610
- // create image an image and add callback
611
- const img = new Image();
612
- img.onload = function () {
613
-
614
- // draw the image
615
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
616
- DOMURL.revokeObjectURL(url);
617
-
618
- // convert canvas to blob
619
- canvas.toBlob(function (blob) {
620
- if (blob) {
621
- resolve(blob);
622
- } else {
623
- reject(new Error('Failed to create blob'));
624
- }
625
- }, 'image/png');
626
- };
627
-
628
- // add error callback
629
- img.onerror = function (err) {
630
- reject(err);
631
- };
632
-
633
- // assign value to run the onload callback
634
- img.src = url;
635
- });
636
-
637
- // resolve promise and return the result
638
- return (await promise);
639
- }