svelte-plots-basic 3.4.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/README.md +31 -14
- package/package.json +4 -5
- package/src/2d/Area.svelte +8 -7
- package/src/2d/Axes.svelte +10 -8
- package/src/2d/AxisLines.svelte +4 -6
- package/src/2d/AxisTickLabels.svelte +1 -3
- package/src/2d/Bars.svelte +30 -9
- package/src/2d/Heatmap.svelte +15 -6
- package/src/2d/Legend.svelte +74 -30
- package/src/2d/Lines.svelte +6 -5
- package/src/2d/Multilines.svelte +9 -8
- package/src/2d/Points.svelte +9 -9
- package/src/2d/Rectangles.svelte +47 -18
- package/src/2d/Segments.svelte +3 -3
- package/src/2d/TextLabels.svelte +12 -10
- package/src/2d/TextLegend.svelte +49 -11
- package/src/2d/YAxis.svelte +1 -1
- package/src/3d/Axes.svelte +5 -7
- 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 +15 -13
- 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 +2 -4
- package/src/3d/YAxis.svelte +4 -6
- package/src/3d/ZAxis.svelte +4 -6
- package/src/ExportDialog.svelte +236 -66
- package/src/methods.js +241 -127
package/src/methods.js
CHANGED
|
@@ -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) {
|
|
337
365
|
console.error('getAxisTicks: provided axis limits are not valid.');
|
|
338
366
|
return undefined;
|
|
339
367
|
}
|
|
340
368
|
|
|
369
|
+
const limValues = [Number(limIn[0]), Number(limIn[1])];
|
|
370
|
+
if (!limValues.every(Number.isFinite) || limValues[0] === limValues[1]) {
|
|
371
|
+
console.error('getAxisTicks: provided axis limits are not valid.');
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
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.');
|
|
@@ -593,26 +642,31 @@ function changeDPI(base64Image, dpi) {
|
|
|
593
642
|
* @param {number} width - desired image width in cm
|
|
594
643
|
* @param {number} height - desired image height in cm
|
|
595
644
|
* @param {number} res - resolution (pixels per inch)
|
|
645
|
+
* @param {Function} [onSerialize] - callback invoked after serialization, or if export preparation fails
|
|
596
646
|
*
|
|
597
647
|
*/
|
|
598
648
|
export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
599
649
|
|
|
600
|
-
if (
|
|
601
|
-
if (
|
|
602
|
-
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;
|
|
603
653
|
|
|
604
|
-
|
|
605
|
-
|
|
654
|
+
width = Number(width);
|
|
655
|
+
height = Number(height);
|
|
656
|
+
res = Number(res);
|
|
657
|
+
|
|
658
|
+
if (!Number.isFinite(width) || width < 1 || width > 30) {
|
|
659
|
+
console.error('Parameter "width" must be a finite number between 1 and 30 (cm).');
|
|
606
660
|
return null;
|
|
607
661
|
}
|
|
608
662
|
|
|
609
|
-
if (height < 1 || height > 30) {
|
|
610
|
-
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).');
|
|
611
665
|
return null;
|
|
612
666
|
}
|
|
613
667
|
|
|
614
|
-
if (res < 50 || res > 1200) {
|
|
615
|
-
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).');
|
|
616
670
|
return null;
|
|
617
671
|
}
|
|
618
672
|
|
|
@@ -621,7 +675,7 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
621
675
|
return null;
|
|
622
676
|
}
|
|
623
677
|
|
|
624
|
-
if (
|
|
678
|
+
if (typeof fileName !== 'string' || fileName.trim() === '') {
|
|
625
679
|
fileName = 'plot';
|
|
626
680
|
}
|
|
627
681
|
|
|
@@ -631,79 +685,105 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
631
685
|
const pxHeight = height / 2.54 * res;
|
|
632
686
|
|
|
633
687
|
setTimeout(() => {
|
|
688
|
+
let url = null;
|
|
689
|
+
let callbackInvoked = false;
|
|
634
690
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
// this is needed if SVG image is larger than the future PNG image
|
|
641
|
-
let specialScale = 1;
|
|
642
|
-
if (scaleFactor < 1) {
|
|
643
|
-
specialScale = scaleFactor;
|
|
644
|
-
scaleFactor = 1;
|
|
691
|
+
function notifyCaller() {
|
|
692
|
+
if (callbackInvoked) return;
|
|
693
|
+
callbackInvoked = true;
|
|
694
|
+
if (typeof onSerialize === 'function') onSerialize();
|
|
645
695
|
}
|
|
646
696
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
// set the canvas size to match the SVG element
|
|
652
|
-
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
653
|
-
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
654
|
-
|
|
655
|
-
// clone the SVG and set export dimensions without changing the live plot
|
|
656
|
-
const exportSvg = svg.cloneNode(true);
|
|
657
|
-
exportSvg.setAttribute('width', svgWidth * scaleFactor);
|
|
658
|
-
exportSvg.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;
|
|
659
701
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
}
|
|
664
706
|
|
|
665
|
-
|
|
666
|
-
const svgXml = new XMLSerializer().serializeToString(exportSvg);
|
|
667
|
-
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
668
|
-
const url = URL.createObjectURL(blob);
|
|
707
|
+
let scaleFactor = Math.max(pxWidth / svgWidth, pxHeight / svgHeight);
|
|
669
708
|
|
|
670
|
-
|
|
671
|
-
|
|
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;
|
|
714
|
+
}
|
|
672
715
|
|
|
673
|
-
|
|
716
|
+
// create a new canvas element
|
|
717
|
+
const canvas = document.createElement('canvas');
|
|
718
|
+
const context = canvas.getContext('2d');
|
|
674
719
|
|
|
675
|
-
|
|
676
|
-
|
|
720
|
+
if (!context) {
|
|
721
|
+
throw new Error('A 2D canvas context is not available.');
|
|
722
|
+
}
|
|
677
723
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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;
|
|
682
771
|
}
|
|
772
|
+
};
|
|
683
773
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// create a data URL for the canvas and adjust its DPI
|
|
687
|
-
const dataURL = canvas.toDataURL('image/png');
|
|
688
|
-
const dataURL2 = changeDPI(dataURL, res);
|
|
689
|
-
|
|
690
|
-
// create a temporary link and trigger the download
|
|
691
|
-
const downloadLink = document.createElement('a');
|
|
692
|
-
downloadLink.href = dataURL2;
|
|
693
|
-
downloadLink.download = `${fileName}.png`;
|
|
694
|
-
downloadLink.click();
|
|
695
|
-
} finally {
|
|
774
|
+
img.onerror = function (error) {
|
|
696
775
|
URL.revokeObjectURL(url);
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
+
}
|
|
707
787
|
}, 20);
|
|
708
788
|
}
|
|
709
789
|
|
|
@@ -756,36 +836,43 @@ export function copyToClipboard(btn, svg, width, height) {
|
|
|
756
836
|
// save button conent
|
|
757
837
|
const content = btn.textContent;
|
|
758
838
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
btn.classList.
|
|
766
|
-
btn.textContent =
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
+
}
|
|
789
876
|
|
|
790
877
|
}
|
|
791
878
|
|
|
@@ -964,7 +1051,7 @@ export function getXAxisParams(limX, limY, scales, tY, axis) {
|
|
|
964
1051
|
let tickFactor = 0;
|
|
965
1052
|
|
|
966
1053
|
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
967
|
-
const ticksX = getAxisTicks(ticks, limX, XTICK_NUM[scales.x],
|
|
1054
|
+
const ticksX = getAxisTicks(ticks, limX, XTICK_NUM[scales.x], false, axis.whole);
|
|
968
1055
|
const tickNum = ticksX.length;
|
|
969
1056
|
|
|
970
1057
|
// compute tick y-coordinates (up and bottom)
|
|
@@ -1048,7 +1135,7 @@ export function getYAxisParams(limX, limY, scales, tX, axis) {
|
|
|
1048
1135
|
let tickFactor = 0;
|
|
1049
1136
|
|
|
1050
1137
|
// compute x-coordinates of the ticks or take the ones manually specified by user
|
|
1051
|
-
const ticksY = getAxisTicks(axis.ticks, limY, YTICK_NUM[scales.y],
|
|
1138
|
+
const ticksY = getAxisTicks(axis.ticks, limY, YTICK_NUM[scales.y], false, axis.whole);
|
|
1052
1139
|
const tickNum = ticksY.length;
|
|
1053
1140
|
|
|
1054
1141
|
// compute tick y-coordinates (up and bottom)
|
|
@@ -1240,12 +1327,11 @@ export function getTextWidth(text, font) {
|
|
|
1240
1327
|
*
|
|
1241
1328
|
* @param {Array} items - array of legend items and their properties.
|
|
1242
1329
|
* @param {number} fontSize - font size of a legend element in pixels.
|
|
1243
|
-
* @param {string}
|
|
1330
|
+
* @param {string} orientation - arrangement of legend items.
|
|
1244
1331
|
*
|
|
1245
|
-
* @return {
|
|
1246
|
-
* height and width of text labels (all in pixels).
|
|
1332
|
+
* @return {Object} coordinates and sizes of legend items (all in pixels).
|
|
1247
1333
|
*/
|
|
1248
|
-
function getLegendSize(items, fontSize) {
|
|
1334
|
+
function getLegendSize(items, fontSize, orientation = 'vertical') {
|
|
1249
1335
|
|
|
1250
1336
|
// TODO: implement dynamic font family
|
|
1251
1337
|
const fontName = 'Arial';
|
|
@@ -1255,25 +1341,49 @@ function getLegendSize(items, fontSize) {
|
|
|
1255
1341
|
const elw = fontSize * 1.5; // width of legend element without label and padding
|
|
1256
1342
|
|
|
1257
1343
|
// compute size of text label elements
|
|
1258
|
-
|
|
1344
|
+
const elx = Array(items.length);
|
|
1259
1345
|
const elh = Array(items.length);
|
|
1260
1346
|
const ely = Array(items.length);
|
|
1261
|
-
|
|
1347
|
+
const eliw = Array(items.length);
|
|
1262
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;
|
|
1263
1371
|
let lbw = 0;
|
|
1264
1372
|
|
|
1265
1373
|
for (let i = 0; i < items.length; i++) {
|
|
1266
1374
|
const w = getTextWidth(' ' + items[i].label + ' ', fontSize + 'px ' + fontName);
|
|
1267
1375
|
lbw = w > lbw ? w : lbw;
|
|
1268
1376
|
const h = fontSize * items[i].labelHeight + 2 * elp;
|
|
1377
|
+
elx[i] = 0;
|
|
1269
1378
|
elh[i] = h;
|
|
1270
1379
|
ely[i] = y;
|
|
1271
1380
|
y = y + h;
|
|
1272
1381
|
lgh = lgh + h;
|
|
1273
1382
|
}
|
|
1274
1383
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1384
|
+
lgw = (elw + 2 * elp + lbw);
|
|
1385
|
+
eliw.fill(lgw);
|
|
1386
|
+
return {elx, ely, eliw, elh, elw, elp, lgh, lgw};
|
|
1277
1387
|
}
|
|
1278
1388
|
|
|
1279
1389
|
|
|
@@ -1288,7 +1398,11 @@ function getLegendSize(items, fontSize) {
|
|
|
1288
1398
|
export function getGroupLegendCoords(params, cpx, cpy, fontSize, ts) {
|
|
1289
1399
|
|
|
1290
1400
|
// compute size of legend elements
|
|
1291
|
-
const
|
|
1401
|
+
const {elx, ely, eliw, elh, elw, elp, lgh, lgw} = getLegendSize(
|
|
1402
|
+
params.items,
|
|
1403
|
+
fontSize * params.fontSize,
|
|
1404
|
+
params.orientation
|
|
1405
|
+
);
|
|
1292
1406
|
|
|
1293
1407
|
// compute coordinates of top left corner of the legend box
|
|
1294
1408
|
const lgl = params.position.includes("left") ? cpx[0] + ts :
|
|
@@ -1300,7 +1414,7 @@ export function getGroupLegendCoords(params, cpx, cpy, fontSize, ts) {
|
|
|
1300
1414
|
(cpy[1] + cpy[0] - lgh) * 0.5;
|
|
1301
1415
|
|
|
1302
1416
|
|
|
1303
|
-
return {lgl, lgt, lgw, lgh, ely, elh, elw, elp};
|
|
1417
|
+
return {lgl, lgt, lgw, lgh, elx, ely, eliw, elh, elw, elp};
|
|
1304
1418
|
}
|
|
1305
1419
|
|
|
1306
1420
|
|
|
@@ -1314,11 +1428,11 @@ export function text2svg(text) {
|
|
|
1314
1428
|
if (!text || text.length < 1) return text;
|
|
1315
1429
|
|
|
1316
1430
|
function toSuper(txt) {
|
|
1317
|
-
return '<tspan font-size="0.
|
|
1431
|
+
return '<tspan font-size="0.6em" dominant-baseline="inherit" baseline-shift="30%">' + txt + '</tspan>';
|
|
1318
1432
|
}
|
|
1319
1433
|
|
|
1320
1434
|
function toSub(txt) {
|
|
1321
|
-
return '<tspan font-size="0.
|
|
1435
|
+
return '<tspan font-size="0.6em" dominant-baseline="inherit" baseline-shift="-50%">' + txt + '</tspan>';
|
|
1322
1436
|
}
|
|
1323
1437
|
|
|
1324
1438
|
let i = 0;
|
|
@@ -1403,7 +1517,7 @@ export function getXAxisCoords3D(xaxis, limX, limY, limZ, scale) {
|
|
|
1403
1517
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1404
1518
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1405
1519
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1406
|
-
const ticksX = getAxisTicks(xaxis.ticks, limX, TICK_NUM[scale],
|
|
1520
|
+
const ticksX = getAxisTicks(xaxis.ticks, limX, TICK_NUM[scale], false);
|
|
1407
1521
|
const tickNum = ticksX.length;
|
|
1408
1522
|
|
|
1409
1523
|
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|
|
@@ -1466,7 +1580,7 @@ export function getYAxisCoords3D(yaxis, limX, limY, limZ, scale) {
|
|
|
1466
1580
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1467
1581
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1468
1582
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1469
|
-
const ticksY = getAxisTicks(yaxis.ticks, limY, TICK_NUM[scale],
|
|
1583
|
+
const ticksY = getAxisTicks(yaxis.ticks, limY, TICK_NUM[scale], false);
|
|
1470
1584
|
const tickNum = ticksY.length;
|
|
1471
1585
|
|
|
1472
1586
|
const [ticksX, ticksX1, ticksX2, ticksX3] = getLatentTicks(limX, tickNum);
|
|
@@ -1529,7 +1643,7 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
|
1529
1643
|
const dX = (limX[1] - limX[0]) / 100; // 1% of axis size
|
|
1530
1644
|
const dY = (limY[1] - limY[0]) / 100; // 1% of axis size
|
|
1531
1645
|
const dZ = (limZ[1] - limZ[0]) / 100; // 1% of axis size
|
|
1532
|
-
const ticksZ = getAxisTicks(zaxis.ticks, limZ, TICK_NUM[scale],
|
|
1646
|
+
const ticksZ = getAxisTicks(zaxis.ticks, limZ, TICK_NUM[scale], false);
|
|
1533
1647
|
const tickNum = ticksZ.length;
|
|
1534
1648
|
|
|
1535
1649
|
const [ticksY, ticksY1, ticksY2, ticksY3] = getLatentTicks(limY, tickNum);
|