svelte-plots-basic 3.3.0 → 4.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/LICENSE +21 -0
- package/README.md +60 -20
- package/package.json +12 -4
- package/src/2d/Area.svelte +8 -9
- package/src/2d/Axes.svelte +78 -20
- package/src/2d/AxisLines.svelte +4 -6
- package/src/2d/AxisTickLabels.svelte +1 -3
- package/src/2d/Bars.svelte +31 -11
- package/src/2d/Box.svelte +5 -2
- package/src/2d/ColormapLegend.svelte +5 -2
- package/src/2d/Heatmap.svelte +98 -25
- package/src/2d/Legend.svelte +83 -31
- package/src/2d/Lines.svelte +6 -7
- package/src/2d/Multilines.svelte +10 -10
- package/src/2d/Points.svelte +9 -10
- package/src/2d/Rectangles.svelte +47 -20
- package/src/2d/Segments.svelte +3 -3
- package/src/2d/TextLabels.svelte +30 -14
- package/src/2d/TextLegend.svelte +52 -6
- package/src/2d/XAxis.svelte +5 -1
- package/src/2d/YAxis.svelte +6 -2
- package/src/3d/Axes.svelte +31 -22
- package/src/3d/AxisLines.svelte +3 -4
- package/src/3d/AxisTickLabels.svelte +2 -2
- package/src/3d/Lines.svelte +8 -8
- package/src/3d/Mesh.svelte +73 -35
- package/src/3d/Points.svelte +8 -9
- package/src/3d/Segments.svelte +11 -13
- package/src/3d/TextLabels.svelte +27 -14
- package/src/3d/XAxis.svelte +7 -6
- package/src/3d/YAxis.svelte +9 -8
- package/src/3d/ZAxis.svelte +9 -8
- package/src/ExportDialog.svelte +237 -65
- package/src/methods.js +342 -176
package/src/methods.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cbind, vector, isvector, Vector, matrix } from 'mdatools/arrays';
|
|
2
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';
|
|
3
|
+
import { TICK_NUM, XTICK_NUM, YTICK_NUM, TICK_SIZE, COLMAP_WIDTH_RATIO, COLMAP_EL_HEIGHTS_PX, COLMAP_TOP_MARGIN } from './constants.js';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -31,6 +31,29 @@ export function checkArray(arr, len) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Validate a line type and fall back to a solid line when it is invalid.
|
|
36
|
+
* Numeric strings are accepted for compatibility with HTML-style component attributes.
|
|
37
|
+
*
|
|
38
|
+
* @param {number|string} lineType - line type (1 - solid, 2 - dashed, 3 - dotted, 4 - dashdot).
|
|
39
|
+
* @param {string} source - name of the component which called the method.
|
|
40
|
+
* @returns {number} normalized line type.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeLineType(lineType, source = 'Plot') {
|
|
43
|
+
const canConvert =
|
|
44
|
+
typeof lineType === 'number' ||
|
|
45
|
+
(typeof lineType === 'string' && lineType.trim() !== '');
|
|
46
|
+
const normalized = canConvert ? Number(lineType) : NaN;
|
|
47
|
+
|
|
48
|
+
if (!Number.isInteger(normalized) || normalized < 1 || normalized > 4) {
|
|
49
|
+
console.error(`${source}: parameter "lineType" must be a whole number from 1 to 4.`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
34
57
|
/**
|
|
35
58
|
* Generic function to transform x or y-values from plot coordinates to screen (SVG) coordinates.
|
|
36
59
|
*
|
|
@@ -94,7 +117,8 @@ export function invTransformObjects(v, t) {
|
|
|
94
117
|
*/
|
|
95
118
|
export function getTickFactorLabel(tf) {
|
|
96
119
|
if (tf === 0) return '';
|
|
97
|
-
return
|
|
120
|
+
return '<tspan dominant-baseline="inherit">×10</tspan>' +
|
|
121
|
+
`<tspan font-size="0.75em" dominant-baseline="inherit" baseline-shift="37.5%">${tf.toString()}</tspan>`;
|
|
98
122
|
}
|
|
99
123
|
|
|
100
124
|
|
|
@@ -167,7 +191,7 @@ export function getTickLabels(ticks) {
|
|
|
167
191
|
|
|
168
192
|
|
|
169
193
|
/**
|
|
170
|
-
* Check coordinates and convert them to vector if necessary.
|
|
194
|
+
* Check coordinates, ensure all values are finite, and convert them to vector if necessary.
|
|
171
195
|
*
|
|
172
196
|
* @param {Array|Vector} x - vector or array with coordinates.
|
|
173
197
|
* @param {string} source - name of component which called the method (needed for error message).
|
|
@@ -196,6 +220,13 @@ export function checkCoords(x, source, len) {
|
|
|
196
220
|
return null;
|
|
197
221
|
}
|
|
198
222
|
|
|
223
|
+
for (let i = 0; i < x.length; i++) {
|
|
224
|
+
if (!Number.isFinite(x.v[i])) {
|
|
225
|
+
console.error(source + ': coordinates must contain only finite numeric values.');
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
199
230
|
return x;
|
|
200
231
|
}
|
|
201
232
|
|
|
@@ -304,7 +335,7 @@ export function validateTicks(ticks, lim) {
|
|
|
304
335
|
* @param {Array} ticks - vector with ticks if alredy available (if not, new will be computed).
|
|
305
336
|
* @param {Array} lim - vector with axis limits tickets must be computed for.
|
|
306
337
|
* @param {number} maxTickNum - maximum number of ticks to compute.
|
|
307
|
-
* @param {boolean} round - round or not the fractions when computing nice numbers for the ticks.
|
|
338
|
+
* @param {boolean} round - round or not the fractions when computing nice numbers for the ticks, default: false.
|
|
308
339
|
* @param {boolean} whole - should the ticks be a whole number.
|
|
309
340
|
* @param {Number} deltaFactor - percent of range to use as internal margins
|
|
310
341
|
*
|
|
@@ -314,7 +345,7 @@ export function validateTicks(ticks, lim) {
|
|
|
314
345
|
export function getAxisTicks(ticks, limIn, maxTickNum, round, whole, deltaFactor) {
|
|
315
346
|
|
|
316
347
|
if (round === undefined || round === null) {
|
|
317
|
-
round =
|
|
348
|
+
round = false;
|
|
318
349
|
}
|
|
319
350
|
|
|
320
351
|
if (whole === undefined || whole === null) {
|
|
@@ -325,19 +356,37 @@ export function getAxisTicks(ticks, limIn, maxTickNum, round, whole, deltaFactor
|
|
|
325
356
|
deltaFactor = 0.0001;
|
|
326
357
|
}
|
|
327
358
|
|
|
328
|
-
// correct limits internally if they are reverse
|
|
329
|
-
const lim = limIn[0] < limIn[1] ? limIn : [limIn[1], limIn[0]];
|
|
330
|
-
|
|
331
359
|
// if ticks are already provided do not recompute them
|
|
332
360
|
// the validation is done in corresponding component, so no need to do it again
|
|
333
361
|
if (ticks) return ticks;
|
|
334
362
|
|
|
335
363
|
// check if limits are ok
|
|
336
|
-
if (typeof(
|
|
364
|
+
if (!limIn || typeof(limIn) !== "object" || limIn[0] === undefined || limIn[1] === undefined) {
|
|
365
|
+
console.error('getAxisTicks: provided axis limits are not valid.');
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const limValues = [Number(limIn[0]), Number(limIn[1])];
|
|
370
|
+
if (!limValues.every(Number.isFinite) || limValues[0] === limValues[1]) {
|
|
337
371
|
console.error('getAxisTicks: provided axis limits are not valid.');
|
|
338
372
|
return undefined;
|
|
339
373
|
}
|
|
340
374
|
|
|
375
|
+
// correct limits internally if they are reverse
|
|
376
|
+
const lim = limValues[0] < limValues[1] ? limValues : [limValues[1], limValues[0]];
|
|
377
|
+
|
|
378
|
+
maxTickNum = Number(maxTickNum);
|
|
379
|
+
if (!Number.isInteger(maxTickNum) || maxTickNum < 1) {
|
|
380
|
+
console.error('getAxisTicks: parameter "maxTickNum" must be a finite positive whole number.');
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
deltaFactor = Number(deltaFactor);
|
|
385
|
+
if (!Number.isFinite(deltaFactor) || deltaFactor < 0 || deltaFactor >= 0.5) {
|
|
386
|
+
console.error('getAxisTicks: parameter "deltaFactor" must be a finite number from 0 up to, but not including, 0.5.');
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
341
390
|
// get range as a nice number and compute min, max and steps for the tick sequence
|
|
342
391
|
const delta = Math.abs((lim[1] - lim[0])) * deltaFactor;
|
|
343
392
|
const localRange = lim[1] - lim[0] - 2 * delta;
|
|
@@ -345,7 +394,7 @@ export function getAxisTicks(ticks, limIn, maxTickNum, round, whole, deltaFactor
|
|
|
345
394
|
const fraction = 1 / Math.pow(10, exponent - 1);
|
|
346
395
|
const range = Math.round(localRange * fraction) / fraction;
|
|
347
396
|
|
|
348
|
-
let tickSpacing = niceNum(range / maxTickNum,
|
|
397
|
+
let tickSpacing = niceNum(range / maxTickNum, round);
|
|
349
398
|
if (whole) {
|
|
350
399
|
if (Math.abs(lim[1] - lim[0]) <= 1) {
|
|
351
400
|
console.error('getAxisTicks: parameter "whole" will be ignored as axis limits are too narrow to fit whole ticks.');
|
|
@@ -486,13 +535,20 @@ function createPngDataTable() {
|
|
|
486
535
|
}
|
|
487
536
|
|
|
488
537
|
|
|
538
|
+
let pngDataTable = null;
|
|
539
|
+
|
|
540
|
+
|
|
489
541
|
/**
|
|
490
542
|
* Calculate CRC for PNG image
|
|
491
543
|
* @param buf image data
|
|
492
544
|
*/
|
|
493
545
|
function calcCrc(buf) {
|
|
494
546
|
let c = -1;
|
|
495
|
-
|
|
547
|
+
|
|
548
|
+
if (!pngDataTable) {
|
|
549
|
+
pngDataTable = createPngDataTable();
|
|
550
|
+
}
|
|
551
|
+
|
|
496
552
|
for (let n = 0; n < buf.length; n++) {
|
|
497
553
|
c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
|
|
498
554
|
}
|
|
@@ -586,26 +642,31 @@ function changeDPI(base64Image, dpi) {
|
|
|
586
642
|
* @param {number} width - desired image width in cm
|
|
587
643
|
* @param {number} height - desired image height in cm
|
|
588
644
|
* @param {number} res - resolution (pixels per inch)
|
|
645
|
+
* @param {Function} [onSerialize] - callback invoked after serialization, or if export preparation fails
|
|
589
646
|
*
|
|
590
647
|
*/
|
|
591
648
|
export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
592
649
|
|
|
593
|
-
if (
|
|
594
|
-
if (
|
|
595
|
-
if (
|
|
650
|
+
if (width === undefined || width === null || width === '') width = 10;
|
|
651
|
+
if (height === undefined || height === null || height === '') height = 10;
|
|
652
|
+
if (res === undefined || res === null || res === '') res = 300;
|
|
653
|
+
|
|
654
|
+
width = Number(width);
|
|
655
|
+
height = Number(height);
|
|
656
|
+
res = Number(res);
|
|
596
657
|
|
|
597
|
-
if (width < 1 || width > 30) {
|
|
598
|
-
console.error('Parameter "width" must be between 1 and 30 (cm).');
|
|
658
|
+
if (!Number.isFinite(width) || width < 1 || width > 30) {
|
|
659
|
+
console.error('Parameter "width" must be a finite number between 1 and 30 (cm).');
|
|
599
660
|
return null;
|
|
600
661
|
}
|
|
601
662
|
|
|
602
|
-
if (height < 1 || height > 30) {
|
|
603
|
-
console.error('Parameter "height" must be between 1 and 30 (cm).');
|
|
663
|
+
if (!Number.isFinite(height) || height < 1 || height > 30) {
|
|
664
|
+
console.error('Parameter "height" must be a finite number between 1 and 30 (cm).');
|
|
604
665
|
return null;
|
|
605
666
|
}
|
|
606
667
|
|
|
607
|
-
if (res < 50 || res > 1200) {
|
|
608
|
-
console.error('Parameter "res" must be between 50 and 1200 (ppi).');
|
|
668
|
+
if (!Number.isFinite(res) || res < 50 || res > 1200) {
|
|
669
|
+
console.error('Parameter "res" must be a finite number between 50 and 1200 (ppi).');
|
|
609
670
|
return null;
|
|
610
671
|
}
|
|
611
672
|
|
|
@@ -614,7 +675,7 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
614
675
|
return null;
|
|
615
676
|
}
|
|
616
677
|
|
|
617
|
-
if (
|
|
678
|
+
if (typeof fileName !== 'string' || fileName.trim() === '') {
|
|
618
679
|
fileName = 'plot';
|
|
619
680
|
}
|
|
620
681
|
|
|
@@ -624,75 +685,105 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
624
685
|
const pxHeight = height / 2.54 * res;
|
|
625
686
|
|
|
626
687
|
setTimeout(() => {
|
|
688
|
+
let url = null;
|
|
689
|
+
let callbackInvoked = false;
|
|
627
690
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// this is needed if SVG image is larger than the future PNG image
|
|
634
|
-
let specialScale = 1;
|
|
635
|
-
if (scaleFactor < 1) {
|
|
636
|
-
specialScale = scaleFactor;
|
|
637
|
-
scaleFactor = 1;
|
|
691
|
+
function notifyCaller() {
|
|
692
|
+
if (callbackInvoked) return;
|
|
693
|
+
callbackInvoked = true;
|
|
694
|
+
if (typeof onSerialize === 'function') onSerialize();
|
|
638
695
|
}
|
|
639
696
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
// set the canvas size to match the SVG element
|
|
645
|
-
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
646
|
-
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
647
|
-
|
|
648
|
-
// save and set corresponding attributes for SVG element
|
|
649
|
-
const origWidth = svg.getAttribute('width');
|
|
650
|
-
const origHeight = svg.getAttribute('height');
|
|
651
|
-
svg.setAttribute('width', svgWidth * scaleFactor);
|
|
652
|
-
svg.setAttribute('height', svgHeight * scaleFactor);
|
|
697
|
+
try {
|
|
698
|
+
// compute canvas size based on SVG size and desired PNG WIDTH in pixels
|
|
699
|
+
const svgHeight = svg.clientHeight;
|
|
700
|
+
const svgWidth = svg.clientWidth;
|
|
653
701
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// draw the SVG onto the canvas
|
|
660
|
-
const svgXml = new XMLSerializer().serializeToString(svg);
|
|
661
|
-
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
662
|
-
const url = URL.createObjectURL(blob);
|
|
663
|
-
|
|
664
|
-
// restore original attributes
|
|
665
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
666
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
667
|
-
|
|
668
|
-
// notify caller that serialization is done (used by ExportDialog to restore container)
|
|
669
|
-
if (onSerialize) onSerialize();
|
|
670
|
-
|
|
671
|
-
const img = new Image();
|
|
702
|
+
if (!Number.isFinite(svgWidth) || svgWidth <= 0 ||
|
|
703
|
+
!Number.isFinite(svgHeight) || svgHeight <= 0) {
|
|
704
|
+
throw new Error('The SVG must have finite, positive dimensions.');
|
|
705
|
+
}
|
|
672
706
|
|
|
673
|
-
|
|
707
|
+
let scaleFactor = Math.max(pxWidth / svgWidth, pxHeight / svgHeight);
|
|
674
708
|
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
709
|
+
// this is needed if SVG image is larger than the future PNG image
|
|
710
|
+
let specialScale = 1;
|
|
711
|
+
if (scaleFactor < 1) {
|
|
712
|
+
specialScale = scaleFactor;
|
|
713
|
+
scaleFactor = 1;
|
|
679
714
|
}
|
|
680
715
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const dataURL = canvas.toDataURL('image/png');
|
|
685
|
-
const dataURL2 = changeDPI(dataURL, res);
|
|
716
|
+
// create a new canvas element
|
|
717
|
+
const canvas = document.createElement('canvas');
|
|
718
|
+
const context = canvas.getContext('2d');
|
|
686
719
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
downloadLink.download = `${fileName}.png`;
|
|
691
|
-
downloadLink.click();
|
|
692
|
-
};
|
|
720
|
+
if (!context) {
|
|
721
|
+
throw new Error('A 2D canvas context is not available.');
|
|
722
|
+
}
|
|
693
723
|
|
|
694
|
-
|
|
695
|
-
|
|
724
|
+
// set the canvas size to match the SVG element
|
|
725
|
+
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
726
|
+
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
727
|
+
|
|
728
|
+
// clone the SVG and set export dimensions without changing the live plot
|
|
729
|
+
const exportSvg = svg.cloneNode(true);
|
|
730
|
+
exportSvg.setAttribute('width', svgWidth * scaleFactor);
|
|
731
|
+
exportSvg.setAttribute('height', svgHeight * scaleFactor);
|
|
732
|
+
|
|
733
|
+
// draw a white background
|
|
734
|
+
context.fillStyle = 'white';
|
|
735
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
736
|
+
context.scale(scaleFactor, scaleFactor)
|
|
737
|
+
|
|
738
|
+
// draw the SVG onto the canvas
|
|
739
|
+
const svgXml = new XMLSerializer().serializeToString(exportSvg);
|
|
740
|
+
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
741
|
+
url = URL.createObjectURL(blob);
|
|
742
|
+
|
|
743
|
+
// notify caller that serialization is done (used by ExportDialog to restore container)
|
|
744
|
+
notifyCaller();
|
|
745
|
+
|
|
746
|
+
const img = new Image();
|
|
747
|
+
|
|
748
|
+
img.onload = function () {
|
|
749
|
+
try {
|
|
750
|
+
|
|
751
|
+
// downscale image if it must be smaller than the current SVG element
|
|
752
|
+
if (specialScale < 1) {
|
|
753
|
+
img.width = img.width * specialScale;
|
|
754
|
+
img.height = img.height * specialScale;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
758
|
+
|
|
759
|
+
// create a data URL for the canvas and adjust its DPI
|
|
760
|
+
const dataURL = canvas.toDataURL('image/png');
|
|
761
|
+
const dataURL2 = changeDPI(dataURL, res);
|
|
762
|
+
|
|
763
|
+
// create a temporary link and trigger the download
|
|
764
|
+
const downloadLink = document.createElement('a');
|
|
765
|
+
downloadLink.href = dataURL2;
|
|
766
|
+
downloadLink.download = `${fileName}.png`;
|
|
767
|
+
downloadLink.click();
|
|
768
|
+
} finally {
|
|
769
|
+
URL.revokeObjectURL(url);
|
|
770
|
+
url = null;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
img.onerror = function (error) {
|
|
775
|
+
URL.revokeObjectURL(url);
|
|
776
|
+
url = null;
|
|
777
|
+
console.error('Failed to load the SVG for PNG export.', error);
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// set the image source
|
|
781
|
+
img.src = url;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (url !== null) URL.revokeObjectURL(url);
|
|
784
|
+
notifyCaller();
|
|
785
|
+
console.error('Failed to prepare the SVG for PNG export.', error);
|
|
786
|
+
}
|
|
696
787
|
}, 20);
|
|
697
788
|
}
|
|
698
789
|
|
|
@@ -718,26 +809,19 @@ export function downloadSVG(svg, fileName) {
|
|
|
718
809
|
const svgHeight = svg.clientHeight;
|
|
719
810
|
const svgWidth = svg.clientWidth;
|
|
720
811
|
|
|
721
|
-
//
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
svg.setAttribute('width', svgWidth + 'px');
|
|
727
|
-
svg.setAttribute('height', svgHeight + 'px');
|
|
728
|
-
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
812
|
+
// clone the SVG and set export dimensions without changing the live plot
|
|
813
|
+
const exportSvg = svg.cloneNode(true);
|
|
814
|
+
exportSvg.setAttribute('width', svgWidth + 'px');
|
|
815
|
+
exportSvg.setAttribute('height', svgHeight + 'px');
|
|
816
|
+
exportSvg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
729
817
|
|
|
730
|
-
const blob = new Blob([
|
|
818
|
+
const blob = new Blob([exportSvg.outerHTML], { type: 'image/svg+xml' });
|
|
731
819
|
const url = window.URL.createObjectURL(blob)
|
|
732
820
|
const a = document.createElement('a')
|
|
733
821
|
a.setAttribute('href', url)
|
|
734
822
|
a.setAttribute('download', `${fileName}.svg`);
|
|
735
823
|
a.click()
|
|
736
|
-
|
|
737
|
-
// restore original attributes
|
|
738
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
739
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
740
|
-
origViewBox ? svg.setAttribute('viewBox', origViewBox) : svg.removeAttribute('viewBox');
|
|
824
|
+
setTimeout(() => window.URL.revokeObjectURL(url), 0);
|
|
741
825
|
}
|
|
742
826
|
|
|
743
827
|
/**
|
|
@@ -752,36 +836,43 @@ export function copyToClipboard(btn, svg, width, height) {
|
|
|
752
836
|
// save button conent
|
|
753
837
|
const content = btn.textContent;
|
|
754
838
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
btn.classList.
|
|
762
|
-
btn.textContent =
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
839
|
+
function showStatus(className, text) {
|
|
840
|
+
btn.classList.add(className);
|
|
841
|
+
btn.textContent = text;
|
|
842
|
+
|
|
843
|
+
// Remove the class after 0.5 seconds and revert the text
|
|
844
|
+
setTimeout(() => {
|
|
845
|
+
btn.classList.remove(className);
|
|
846
|
+
btn.textContent = content;
|
|
847
|
+
}, 500);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function showError(error) {
|
|
851
|
+
console.error(error);
|
|
852
|
+
showStatus('error', 'x copy');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (typeof navigator === 'undefined' ||
|
|
856
|
+
!navigator.clipboard ||
|
|
857
|
+
typeof navigator.clipboard.write !== 'function' ||
|
|
858
|
+
typeof ClipboardItem === 'undefined') {
|
|
859
|
+
showError(new Error('Copying images to the clipboard is not supported in this environment.'));
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
navigator.clipboard.write([
|
|
865
|
+
new ClipboardItem({ 'image/png': createPngBlob(svg, width, height) })
|
|
866
|
+
])
|
|
867
|
+
.then(
|
|
868
|
+
() => showStatus('copied', '✓ done'),
|
|
869
|
+
showError
|
|
870
|
+
)
|
|
871
|
+
.catch(showError);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
showError(error);
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
785
876
|
|
|
786
877
|
}
|
|
787
878
|
|
|
@@ -813,16 +904,11 @@ export async function createPngBlob(svg, width, height) {
|
|
|
813
904
|
// scale the context
|
|
814
905
|
context.scale(scaleFactor, scaleFactor)
|
|
815
906
|
|
|
816
|
-
//
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
// serialize SVG with export dimensions, then restore original attributes
|
|
823
|
-
const svgData = new XMLSerializer().serializeToString(svg);
|
|
824
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
825
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
907
|
+
// clone and serialize the SVG with export dimensions without changing the live plot
|
|
908
|
+
const exportSvg = svg.cloneNode(true);
|
|
909
|
+
exportSvg.setAttribute('width', canvas.width);
|
|
910
|
+
exportSvg.setAttribute('height', canvas.height);
|
|
911
|
+
const svgData = new XMLSerializer().serializeToString(exportSvg);
|
|
826
912
|
|
|
827
913
|
// create promise
|
|
828
914
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -852,6 +938,7 @@ export async function createPngBlob(svg, width, height) {
|
|
|
852
938
|
|
|
853
939
|
// add error callback
|
|
854
940
|
img.onerror = function (err) {
|
|
941
|
+
DOMURL.revokeObjectURL(url);
|
|
855
942
|
reject(err);
|
|
856
943
|
};
|
|
857
944
|
|
|
@@ -864,6 +951,66 @@ export async function createPngBlob(svg, width, height) {
|
|
|
864
951
|
}
|
|
865
952
|
|
|
866
953
|
|
|
954
|
+
let colmaps = null;
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
function getColmaps() {
|
|
958
|
+
if (!colmaps) {
|
|
959
|
+
colmaps = [
|
|
960
|
+
["#2679B2"],
|
|
961
|
+
["#2679B2", "#D22C2F"],
|
|
962
|
+
["#2679B2", "#92B42A", "#D22C2F"],
|
|
963
|
+
["#2679B2", "#2E9658", "#F2B825", "#D22C2F"],
|
|
964
|
+
["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
|
|
965
|
+
["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
|
|
966
|
+
["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
|
|
967
|
+
["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
|
|
968
|
+
["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
|
|
969
|
+
["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
|
|
970
|
+
["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
|
|
971
|
+
["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
|
|
972
|
+
["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
|
|
973
|
+
["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
|
|
974
|
+
["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
|
|
975
|
+
["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
|
|
976
|
+
];
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return colmaps;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
function interpolateColor(start, end, amount) {
|
|
984
|
+
const channels = [1, 3, 5].map(offset => {
|
|
985
|
+
const from = parseInt(start.slice(offset, offset + 2), 16);
|
|
986
|
+
const to = parseInt(end.slice(offset, offset + 2), 16);
|
|
987
|
+
|
|
988
|
+
return Math.round(from + (to - from) * amount)
|
|
989
|
+
.toString(16)
|
|
990
|
+
.padStart(2, '0');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
return `#${channels.join('')}`.toUpperCase();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
function expandColmap(colors, length) {
|
|
998
|
+
const last = colors.length - 1;
|
|
999
|
+
|
|
1000
|
+
return Array.from({length}, (_, index) => {
|
|
1001
|
+
const position = index * last / (length - 1);
|
|
1002
|
+
const start = Math.floor(position);
|
|
1003
|
+
const end = Math.min(start + 1, last);
|
|
1004
|
+
|
|
1005
|
+
return interpolateColor(
|
|
1006
|
+
colors[start],
|
|
1007
|
+
colors[end],
|
|
1008
|
+
position - start
|
|
1009
|
+
);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
|
|
867
1014
|
/**
|
|
868
1015
|
* Returns an array with 'n' colors.
|
|
869
1016
|
*
|
|
@@ -874,27 +1021,14 @@ export async function createPngBlob(svg, width, height) {
|
|
|
874
1021
|
*
|
|
875
1022
|
*/
|
|
876
1023
|
export function getcolmap(n, alpha) {
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
[
|
|
880
|
-
[
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
|
|
884
|
-
["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
|
|
885
|
-
["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
|
|
886
|
-
["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
|
|
887
|
-
["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
|
|
888
|
-
["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
|
|
889
|
-
["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
|
|
890
|
-
["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
|
|
891
|
-
["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
|
|
892
|
-
["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
|
|
893
|
-
["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
|
|
894
|
-
["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
|
|
895
|
-
]
|
|
1024
|
+
const palettes = getColmaps();
|
|
1025
|
+
const colors = n <= 16
|
|
1026
|
+
? palettes[n - 1]?.slice()
|
|
1027
|
+
: expandColmap(palettes[15], n);
|
|
1028
|
+
|
|
1029
|
+
if (!colors) return colors;
|
|
896
1030
|
|
|
897
|
-
return alpha ?
|
|
1031
|
+
return alpha ? colors.map(v => v + alpha) : colors;
|
|
898
1032
|
}
|
|
899
1033
|
|
|
900
1034
|
|
|
@@ -917,7 +1051,7 @@ export function getXAxisParams(limX, limY, scales, tY, axis) {
|
|
|
917
1051
|
let tickFactor = 0;
|
|
918
1052
|
|
|
919
1053
|
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
920
|
-
const ticksX = getAxisTicks(ticks, limX, XTICK_NUM[scales.x],
|
|
1054
|
+
const ticksX = getAxisTicks(ticks, limX, XTICK_NUM[scales.x], false, axis.whole);
|
|
921
1055
|
const tickNum = ticksX.length;
|
|
922
1056
|
|
|
923
1057
|
// compute tick y-coordinates (up and bottom)
|
|
@@ -1001,7 +1135,7 @@ export function getYAxisParams(limX, limY, scales, tX, axis) {
|
|
|
1001
1135
|
let tickFactor = 0;
|
|
1002
1136
|
|
|
1003
1137
|
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
1004
|
-
const ticksY = getAxisTicks(axis.ticks, limY, YTICK_NUM[scales.y],
|
|
1138
|
+
const ticksY = getAxisTicks(axis.ticks, limY, YTICK_NUM[scales.y], false, axis.whole);
|
|
1005
1139
|
const tickNum = ticksY.length;
|
|
1006
1140
|
|
|
1007
1141
|
// compute tick y-coordinates (up and bottom)
|
|
@@ -1175,11 +1309,16 @@ export function handleClick(e, tagName, onclickCallback) {
|
|
|
1175
1309
|
*
|
|
1176
1310
|
* @return {number} size of element in pixels
|
|
1177
1311
|
*/
|
|
1312
|
+
let textMeasureContext = null;
|
|
1313
|
+
|
|
1178
1314
|
export function getTextWidth(text, font) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1315
|
+
if (!textMeasureContext) {
|
|
1316
|
+
const canvas = document.createElement('canvas');
|
|
1317
|
+
textMeasureContext = canvas.getContext('2d');
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
textMeasureContext.font = font;
|
|
1321
|
+
return textMeasureContext.measureText(text.replace(/<(.|\n)*?>/g, '')).width;
|
|
1183
1322
|
}
|
|
1184
1323
|
|
|
1185
1324
|
|
|
@@ -1188,12 +1327,11 @@ export function getTextWidth(text, font) {
|
|
|
1188
1327
|
*
|
|
1189
1328
|
* @param {Array} items - array of legend items and their properties.
|
|
1190
1329
|
* @param {number} fontSize - font size of a legend element in pixels.
|
|
1191
|
-
* @param {string}
|
|
1330
|
+
* @param {string} orientation - arrangement of legend items.
|
|
1192
1331
|
*
|
|
1193
|
-
* @return {
|
|
1194
|
-
* height and width of text labels (all in pixels).
|
|
1332
|
+
* @return {Object} coordinates and sizes of legend items (all in pixels).
|
|
1195
1333
|
*/
|
|
1196
|
-
function getLegendSize(items, fontSize) {
|
|
1334
|
+
function getLegendSize(items, fontSize, orientation = 'vertical') {
|
|
1197
1335
|
|
|
1198
1336
|
// TODO: implement dynamic font family
|
|
1199
1337
|
const fontName = 'Arial';
|
|
@@ -1203,25 +1341,49 @@ function getLegendSize(items, fontSize) {
|
|
|
1203
1341
|
const elw = fontSize * 1.5; // width of legend element without label and padding
|
|
1204
1342
|
|
|
1205
1343
|
// compute size of text label elements
|
|
1206
|
-
|
|
1344
|
+
const elx = Array(items.length);
|
|
1207
1345
|
const elh = Array(items.length);
|
|
1208
1346
|
const ely = Array(items.length);
|
|
1209
|
-
|
|
1347
|
+
const eliw = Array(items.length);
|
|
1210
1348
|
let lgh = 0;
|
|
1349
|
+
let lgw = 0;
|
|
1350
|
+
|
|
1351
|
+
if (orientation === 'horizontal') {
|
|
1352
|
+
for (let i = 0; i < items.length; i++) {
|
|
1353
|
+
const labelWidth = getTextWidth(' ' + items[i].label + ' ', fontSize + 'px ' + fontName);
|
|
1354
|
+
const itemWidth = elw + 2 * elp + labelWidth;
|
|
1355
|
+
const itemHeight = fontSize * items[i].labelHeight + 2 * elp;
|
|
1356
|
+
|
|
1357
|
+
elx[i] = lgw;
|
|
1358
|
+
ely[i] = 0;
|
|
1359
|
+
eliw[i] = itemWidth;
|
|
1360
|
+
elh[i] = itemHeight;
|
|
1361
|
+
lgw = lgw + itemWidth;
|
|
1362
|
+
lgh = itemHeight > lgh ? itemHeight : lgh;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Use one common row height so all item centers are aligned.
|
|
1366
|
+
elh.fill(lgh);
|
|
1367
|
+
return {elx, ely, eliw, elh, elw, elp, lgh, lgw};
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
let y = 0;
|
|
1211
1371
|
let lbw = 0;
|
|
1212
1372
|
|
|
1213
1373
|
for (let i = 0; i < items.length; i++) {
|
|
1214
1374
|
const w = getTextWidth(' ' + items[i].label + ' ', fontSize + 'px ' + fontName);
|
|
1215
1375
|
lbw = w > lbw ? w : lbw;
|
|
1216
1376
|
const h = fontSize * items[i].labelHeight + 2 * elp;
|
|
1377
|
+
elx[i] = 0;
|
|
1217
1378
|
elh[i] = h;
|
|
1218
1379
|
ely[i] = y;
|
|
1219
1380
|
y = y + h;
|
|
1220
1381
|
lgh = lgh + h;
|
|
1221
1382
|
}
|
|
1222
1383
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1384
|
+
lgw = (elw + 2 * elp + lbw);
|
|
1385
|
+
eliw.fill(lgw);
|
|
1386
|
+
return {elx, ely, eliw, elh, elw, elp, lgh, lgw};
|
|
1225
1387
|
}
|
|
1226
1388
|
|
|
1227
1389
|
|
|
@@ -1236,7 +1398,11 @@ function getLegendSize(items, fontSize) {
|
|
|
1236
1398
|
export function getGroupLegendCoords(params, cpx, cpy, fontSize, ts) {
|
|
1237
1399
|
|
|
1238
1400
|
// compute size of legend elements
|
|
1239
|
-
const
|
|
1401
|
+
const {elx, ely, eliw, elh, elw, elp, lgh, lgw} = getLegendSize(
|
|
1402
|
+
params.items,
|
|
1403
|
+
fontSize * params.fontSize,
|
|
1404
|
+
params.orientation
|
|
1405
|
+
);
|
|
1240
1406
|
|
|
1241
1407
|
// compute coordinates of top left corner of the legend box
|
|
1242
1408
|
const lgl = params.position.includes("left") ? cpx[0] + ts :
|
|
@@ -1248,7 +1414,7 @@ export function getGroupLegendCoords(params, cpx, cpy, fontSize, ts) {
|
|
|
1248
1414
|
(cpy[1] + cpy[0] - lgh) * 0.5;
|
|
1249
1415
|
|
|
1250
1416
|
|
|
1251
|
-
return {lgl, lgt, lgw, lgh, ely, elh, elw, elp};
|
|
1417
|
+
return {lgl, lgt, lgw, lgh, elx, ely, eliw, elh, elw, elp};
|
|
1252
1418
|
}
|
|
1253
1419
|
|
|
1254
1420
|
|
|
@@ -1262,11 +1428,11 @@ export function text2svg(text) {
|
|
|
1262
1428
|
if (!text || text.length < 1) return text;
|
|
1263
1429
|
|
|
1264
1430
|
function toSuper(txt) {
|
|
1265
|
-
return '<tspan font-size="0.
|
|
1431
|
+
return '<tspan font-size="0.6em" dominant-baseline="inherit" baseline-shift="30%">' + txt + '</tspan>';
|
|
1266
1432
|
}
|
|
1267
1433
|
|
|
1268
1434
|
function toSub(txt) {
|
|
1269
|
-
return '<tspan font-size="0.
|
|
1435
|
+
return '<tspan font-size="0.6em" dominant-baseline="inherit" baseline-shift="-50%">' + txt + '</tspan>';
|
|
1270
1436
|
}
|
|
1271
1437
|
|
|
1272
1438
|
let i = 0;
|
|
@@ -1351,7 +1517,7 @@ export function getXAxisCoords3D(xaxis, limX, limY, limZ, scale) {
|
|
|
1351
1517
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1352
1518
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1353
1519
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1354
|
-
const ticksX = getAxisTicks(xaxis.ticks, limX, TICK_NUM[scale],
|
|
1520
|
+
const ticksX = getAxisTicks(xaxis.ticks, limX, TICK_NUM[scale], false);
|
|
1355
1521
|
const tickNum = ticksX.length;
|
|
1356
1522
|
|
|
1357
1523
|
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|
|
@@ -1414,7 +1580,7 @@ export function getYAxisCoords3D(yaxis, limX, limY, limZ, scale) {
|
|
|
1414
1580
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1415
1581
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1416
1582
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1417
|
-
const ticksY = getAxisTicks(yaxis.ticks, limY, TICK_NUM[scale],
|
|
1583
|
+
const ticksY = getAxisTicks(yaxis.ticks, limY, TICK_NUM[scale], false);
|
|
1418
1584
|
const tickNum = ticksY.length;
|
|
1419
1585
|
|
|
1420
1586
|
const [ticksX, ticksX1, ticksX2, ticksX3] = getLatentTicks(limX, tickNum);
|
|
@@ -1477,7 +1643,7 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
|
1477
1643
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1478
1644
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1479
1645
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1480
|
-
const ticksZ = getAxisTicks(zaxis.ticks, limZ, TICK_NUM[scale],
|
|
1646
|
+
const ticksZ = getAxisTicks(zaxis.ticks, limZ, TICK_NUM[scale], false);
|
|
1481
1647
|
const tickNum = ticksZ.length;
|
|
1482
1648
|
|
|
1483
1649
|
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|
|
@@ -1523,4 +1689,4 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
|
1523
1689
|
];
|
|
1524
1690
|
|
|
1525
1691
|
return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
|
|
1526
|
-
}
|
|
1692
|
+
}
|