svelte-plots-basic 3.2.1 → 3.4.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
@@ -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
  /**
@@ -290,7 +290,7 @@ export function validateTicks(ticks, lim) {
290
290
  const right = lim[0] < lim[1] ? lim[1] : lim[0];
291
291
 
292
292
  // check if provided ticks values are outside the axis limit range continue with automatic ticks
293
- const newTicks = ticks.filter(x => x >= left & x <= right);
293
+ const newTicks = ticks.filter(x => x >= left && x <= right);
294
294
  if (newTicks.length !== ticks.length) {
295
295
  return [null, 'some of ticks values are outside axis limits.'];
296
296
  }
@@ -381,7 +381,7 @@ export function getAxisTicks(ticks, limIn, maxTickNum, round, whole, deltaFactor
381
381
  }
382
382
 
383
383
  // make sure the ticks are not aligned with axes limits
384
- return ticks.filter(x => x >= lim[0] & x <= lim[1]);
384
+ return ticks.filter(x => x >= lim[0] && x <= lim[1]);
385
385
  }
386
386
 
387
387
 
@@ -456,7 +456,7 @@ export function getScale(width, height) {
456
456
  * @returns {text} the scale level ("small", "medium" or "large").
457
457
  *
458
458
  */
459
- export function getAxisScale(width, size) {
459
+ export function getAxisScale(width) {
460
460
  if (width < 400.2) return "small";
461
461
  if (width < 700.2) return "medium";
462
462
  if (width < 900.2) return "large";
@@ -486,13 +486,20 @@ function createPngDataTable() {
486
486
  }
487
487
 
488
488
 
489
+ let pngDataTable = null;
490
+
491
+
489
492
  /**
490
493
  * Calculate CRC for PNG image
491
494
  * @param buf image data
492
495
  */
493
496
  function calcCrc(buf) {
494
497
  let c = -1;
495
- let pngDataTable = createPngDataTable();
498
+
499
+ if (!pngDataTable) {
500
+ pngDataTable = createPngDataTable();
501
+ }
502
+
496
503
  for (let n = 0; n < buf.length; n++) {
497
504
  c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
498
505
  }
@@ -588,7 +595,7 @@ function changeDPI(base64Image, dpi) {
588
595
  * @param {number} res - resolution (pixels per inch)
589
596
  *
590
597
  */
591
- export function downloadPNG (svg, fileName, width, height, res) {
598
+ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
592
599
 
593
600
  if (!width) width = 10;
594
601
  if (!height) height = 10;
@@ -645,9 +652,10 @@ export function downloadPNG (svg, fileName, width, height, res) {
645
652
  canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
646
653
  canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
647
654
 
648
- // set corresponding attributes for SVG element
649
- svg.setAttribute('width', svgWidth * scaleFactor);
650
- svg.setAttribute('height', svgHeight * scaleFactor);
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);
651
659
 
652
660
  // draw a white background
653
661
  context.fillStyle = 'white';
@@ -655,31 +663,43 @@ export function downloadPNG (svg, fileName, width, height, res) {
655
663
  context.scale(scaleFactor, scaleFactor)
656
664
 
657
665
  // draw the SVG onto the canvas
658
- const svgXml = new XMLSerializer().serializeToString(svg);
666
+ const svgXml = new XMLSerializer().serializeToString(exportSvg);
659
667
  const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
660
668
  const url = URL.createObjectURL(blob);
661
669
 
670
+ // notify caller that serialization is done (used by ExportDialog to restore container)
671
+ if (onSerialize) onSerialize();
672
+
662
673
  const img = new Image();
663
674
 
664
675
  img.onload = function () {
676
+ try {
665
677
 
666
- // downscale image if it must be smaller than the current SVG element
667
- if (specialScale < 0) {
668
- img.width = img.width * specialScale;
669
- img.height = img.height * specialScale;
670
- }
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;
682
+ }
671
683
 
672
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
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);
673
689
 
674
- // create a data URL for the canvas and adjust its DPI
675
- const dataURL = canvas.toDataURL('image/png');
676
- const dataURL2 = changeDPI(dataURL, res);
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 {
696
+ URL.revokeObjectURL(url);
697
+ }
698
+ };
677
699
 
678
- // create a temporary link and trigger the download
679
- const downloadLink = document.createElement('a');
680
- downloadLink.href = dataURL2;
681
- downloadLink.download = `${fileName}.png`;
682
- downloadLink.click();
700
+ img.onerror = function (error) {
701
+ URL.revokeObjectURL(url);
702
+ console.error('Failed to load the SVG for PNG export.', error);
683
703
  };
684
704
 
685
705
  // set the image source
@@ -708,16 +728,20 @@ export function downloadSVG(svg, fileName) {
708
728
 
709
729
  const svgHeight = svg.clientHeight;
710
730
  const svgWidth = svg.clientWidth;
711
- svg.setAttribute('width', svgWidth + 'px');
712
- svg.setAttribute('height', svgHeight + 'px');
713
- svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
714
731
 
715
- const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml' });
732
+ // clone the SVG and set export dimensions without changing the live plot
733
+ const exportSvg = svg.cloneNode(true);
734
+ exportSvg.setAttribute('width', svgWidth + 'px');
735
+ exportSvg.setAttribute('height', svgHeight + 'px');
736
+ exportSvg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
737
+
738
+ const blob = new Blob([exportSvg.outerHTML], { type: 'image/svg+xml' });
716
739
  const url = window.URL.createObjectURL(blob)
717
740
  const a = document.createElement('a')
718
741
  a.setAttribute('href', url)
719
742
  a.setAttribute('download', `${fileName}.svg`);
720
743
  a.click()
744
+ setTimeout(() => window.URL.revokeObjectURL(url), 0);
721
745
  }
722
746
 
723
747
  /**
@@ -793,15 +817,16 @@ export async function createPngBlob(svg, width, height) {
793
817
  // scale the context
794
818
  context.scale(scaleFactor, scaleFactor)
795
819
 
796
- // set corresponding attributes for SVG element
797
- svg.setAttribute('width', canvas.width);
798
- svg.setAttribute('height', canvas.height);
820
+ // clone and serialize the SVG with export dimensions without changing the live plot
821
+ const exportSvg = svg.cloneNode(true);
822
+ exportSvg.setAttribute('width', canvas.width);
823
+ exportSvg.setAttribute('height', canvas.height);
824
+ const svgData = new XMLSerializer().serializeToString(exportSvg);
799
825
 
800
826
  // create promise
801
827
  const promise = new Promise((resolve, reject) => {
802
828
 
803
- // serialzie SVG, create blob and URL for it
804
- const svgData = new XMLSerializer().serializeToString(svg);
829
+ // create blob and URL for serialized SVG
805
830
  const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
806
831
  const DOMURL = window.URL || window.webkitURL || window;
807
832
  const url = DOMURL.createObjectURL(svgBlob);
@@ -826,6 +851,7 @@ export async function createPngBlob(svg, width, height) {
826
851
 
827
852
  // add error callback
828
853
  img.onerror = function (err) {
854
+ DOMURL.revokeObjectURL(url);
829
855
  reject(err);
830
856
  };
831
857
 
@@ -838,6 +864,66 @@ export async function createPngBlob(svg, width, height) {
838
864
  }
839
865
 
840
866
 
867
+ let colmaps = null;
868
+
869
+
870
+ function getColmaps() {
871
+ if (!colmaps) {
872
+ colmaps = [
873
+ ["#2679B2"],
874
+ ["#2679B2", "#D22C2F"],
875
+ ["#2679B2", "#92B42A", "#D22C2F"],
876
+ ["#2679B2", "#2E9658", "#F2B825", "#D22C2F"],
877
+ ["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
878
+ ["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
879
+ ["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
880
+ ["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
881
+ ["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
882
+ ["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
883
+ ["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
884
+ ["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
885
+ ["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
886
+ ["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
887
+ ["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
888
+ ["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
889
+ ];
890
+ }
891
+
892
+ return colmaps;
893
+ }
894
+
895
+
896
+ function interpolateColor(start, end, amount) {
897
+ const channels = [1, 3, 5].map(offset => {
898
+ const from = parseInt(start.slice(offset, offset + 2), 16);
899
+ const to = parseInt(end.slice(offset, offset + 2), 16);
900
+
901
+ return Math.round(from + (to - from) * amount)
902
+ .toString(16)
903
+ .padStart(2, '0');
904
+ });
905
+
906
+ return `#${channels.join('')}`.toUpperCase();
907
+ }
908
+
909
+
910
+ function expandColmap(colors, length) {
911
+ const last = colors.length - 1;
912
+
913
+ return Array.from({length}, (_, index) => {
914
+ const position = index * last / (length - 1);
915
+ const start = Math.floor(position);
916
+ const end = Math.min(start + 1, last);
917
+
918
+ return interpolateColor(
919
+ colors[start],
920
+ colors[end],
921
+ position - start
922
+ );
923
+ });
924
+ }
925
+
926
+
841
927
  /**
842
928
  * Returns an array with 'n' colors.
843
929
  *
@@ -848,27 +934,14 @@ export async function createPngBlob(svg, width, height) {
848
934
  *
849
935
  */
850
936
  export function getcolmap(n, alpha) {
851
- if (n > 16) n = 16;
852
- const colmap = [
853
- ["#2679B2"],
854
- ["#2679B2", "#D22C2F"],
855
- ["#2679B2", "#92B42A", "#D22C2F"],
856
- ["#2679B2", "#2E9658", "#F2B825", "#D22C2F"],
857
- ["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
858
- ["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
859
- ["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
860
- ["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
861
- ["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
862
- ["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
863
- ["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
864
- ["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
865
- ["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
866
- ["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
867
- ["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
868
- ["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
869
- ]
937
+ const palettes = getColmaps();
938
+ const colors = n <= 16
939
+ ? palettes[n - 1]?.slice()
940
+ : expandColmap(palettes[15], n);
941
+
942
+ if (!colors) return colors;
870
943
 
871
- return alpha ? colmap[n - 1].map(v => v + alpha) : colmap[n - 1];
944
+ return alpha ? colors.map(v => v + alpha) : colors;
872
945
  }
873
946
 
874
947
 
@@ -1035,10 +1108,6 @@ export function getYAxisParams(limX, limY, scales, tX, axis) {
1035
1108
  tickLabels[pos] = ' ';
1036
1109
  }
1037
1110
 
1038
- // old version of the code above - remove after testing
1039
- // if (tfLabel.length > 0 && (limY[1] - ticksY.v[ticksY.length - 1]) < ((limY[1] - limY[0]) * 0.05)) {
1040
- // tickLabels[tickLabels.length - 1] = ' ';
1041
- // }
1042
1111
 
1043
1112
  return {grid, axisLine, tickCoords, tfCoords, ticks, tickLabels, tickFactor, tfLabel};
1044
1113
  }
@@ -1153,11 +1222,16 @@ export function handleClick(e, tagName, onclickCallback) {
1153
1222
  *
1154
1223
  * @return {number} size of element in pixels
1155
1224
  */
1225
+ let textMeasureContext = null;
1226
+
1156
1227
  export function getTextWidth(text, font) {
1157
- const element = document.createElement('canvas');
1158
- const context = element.getContext('2d');
1159
- context.font = font;
1160
- return context.measureText(text.replace(/<(.|\n)*?>/g, '')).width;
1228
+ if (!textMeasureContext) {
1229
+ const canvas = document.createElement('canvas');
1230
+ textMeasureContext = canvas.getContext('2d');
1231
+ }
1232
+
1233
+ textMeasureContext.font = font;
1234
+ return textMeasureContext.measureText(text.replace(/<(.|\n)*?>/g, '')).width;
1161
1235
  }
1162
1236
 
1163
1237
 
@@ -1248,8 +1322,6 @@ export function text2svg(text) {
1248
1322
  }
1249
1323
 
1250
1324
  let i = 0;
1251
- const dy = 0.5;
1252
- const fs = 0.8;
1253
1325
  let svgText = '';
1254
1326
  while (i < text.length) {
1255
1327
 
@@ -1445,7 +1517,7 @@ export function getYAxisCoords3D(yaxis, limX, limY, limZ, scale) {
1445
1517
 
1446
1518
  /**
1447
1519
  * Computes coordinates of all elements (lines, labels, etc) for z-axis ticks.
1448
- * @param {Object} yaxis - JSON with z-axis paramaters.
1520
+ * @param {Object} zaxis - JSON with z-axis paramaters.
1449
1521
  * @param {Array} limX - array with x-axis limits.
1450
1522
  * @param {Array} limY - array with y-axis limits.
1451
1523
  * @param {Array} limZ - array with z-axis limits.
@@ -1469,8 +1541,8 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
1469
1541
 
1470
1542
  // tick labels
1471
1543
  const tickLabels = (!zaxis.ticks || !zaxis.tickLabels) ? ticksZ.v : zaxis.tickLabels;
1472
- if (tickLabels.length !== ticksX.length) {
1473
- console.error('XAxis (3D): "tickLabels" must be a array of the same size as ticks.');
1544
+ if (tickLabels.length !== ticksZ.length) {
1545
+ console.error('ZAxis (3D): "tickLabels" must be a array of the same size as ticks.');
1474
1546
  }
1475
1547
 
1476
1548
  // combine all coordinates together
@@ -1503,4 +1575,4 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
1503
1575
  ];
1504
1576
 
1505
1577
  return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
1506
- }
1578
+ }