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/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 `<tspan>&times;10</tspan><tspan font-size="0.75em" dy="-0.5em">${(tf).toString()}</tspan>`;
120
+ return '<tspan dominant-baseline="inherit">&times;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 = true;
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(lim) !== "object" || lim[0] === undefined || lim[1] === undefined) {
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, false);
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 (!width) width = 10;
601
- if (!height) height = 10;
602
- if (!res) res = 300;
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
- if (width < 1 || width > 30) {
605
- console.error('Parameter "width" must be between 1 and 30 (cm).');
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 (!fileName || fileName.trim() === '') {
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
- // compute canvas size based on SVG size and desired PNG WIDTH in pixels
636
- const svgHeight = svg.clientHeight;
637
- const svgWidth = svg.clientWidth;
638
- let scaleFactor = Math.max(pxWidth / svgWidth, pxHeight / svgHeight);
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
- // create a new canvas element
648
- const canvas = document.createElement('canvas');
649
- const context = canvas.getContext('2d');
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
- // draw a white background
661
- context.fillStyle = 'white';
662
- context.fillRect(0, 0, canvas.width, canvas.height);
663
- context.scale(scaleFactor, scaleFactor)
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
- // draw the SVG onto the canvas
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
- // notify caller that serialization is done (used by ExportDialog to restore container)
671
- if (onSerialize) onSerialize();
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
- const img = new Image();
716
+ // create a new canvas element
717
+ const canvas = document.createElement('canvas');
718
+ const context = canvas.getContext('2d');
674
719
 
675
- img.onload = function () {
676
- try {
720
+ if (!context) {
721
+ throw new Error('A 2D canvas context is not available.');
722
+ }
677
723
 
678
- // downscale image if it must be smaller than the current SVG element
679
- if (specialScale < 1) {
680
- img.width = img.width * specialScale;
681
- img.height = img.height * specialScale;
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
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
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
- img.onerror = function (error) {
701
- URL.revokeObjectURL(url);
702
- console.error('Failed to load the SVG for PNG export.', error);
703
- };
704
-
705
- // set the image source
706
- img.src = 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
+ }
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
- navigator.clipboard.write([
760
- new ClipboardItem({ 'image/png': createPngBlob(svg, width, height) })
761
- ])
762
- .then(
763
- () => {
764
- // add class "copied" for visual confirmation
765
- btn.classList.add('copied');
766
- btn.textContent = '✓ done';
767
-
768
- // Remove the class after 0.5 seconds and revert the text
769
- setTimeout(() => {
770
- btn.classList.remove('copied');
771
- btn.textContent = content;
772
- }, 500);
773
- },
774
- (e) => {
775
- console.error(e)
776
-
777
- // add class "error" for visual confirmation
778
- btn.classList.add('error');
779
- btn.textContent = 'x copy';
780
-
781
- // Remove the class after 0.5 seconds and revert the text
782
- setTimeout(() => {
783
- btn.classList.remove('error');
784
- btn.textContent = content;
785
- }, 500);
786
- }
787
- )
788
- .catch((error) => console.error(error));
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], true, axis.whole);
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], true, axis.whole);
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} position - position of the legend.
1330
+ * @param {string} orientation - arrangement of legend items.
1244
1331
  *
1245
- * @return {Array} array with height and width of graphical part of legend item, padding size,
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
- // TODO: implement one row legend if position is "top" or "bottom"
1344
+ const elx = Array(items.length);
1259
1345
  const elh = Array(items.length);
1260
1346
  const ely = Array(items.length);
1261
- let y = 0;
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
- const lgw = (elw + 2 * elp + lbw);
1276
- return [ely, elh, elw, elp, lgh, lgw];
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 [ely, elh, elw, elp, lgh, lgw] = getLegendSize(params.items, fontSize * params.fontSize);
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.75em" dy="-0.5em">' + txt + '</tspan><tspan dy="0.4em"> </tspan>';
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.75em" dy="0.5em">' + txt + '</tspan><tspan dy="-0.4em"> </tspan>';
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], true);
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], true);
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], true);
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);