react-msaview 4.4.6 → 4.5.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.
Files changed (118) hide show
  1. package/bundle/index.js +9 -9
  2. package/bundle/index.js.LICENSE.txt +8 -8
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.d.ts +0 -6
  5. package/dist/colorSchemes.js +1 -119
  6. package/dist/colorSchemes.js.map +1 -1
  7. package/dist/components/ConservationTrack.d.ts +8 -0
  8. package/dist/components/ConservationTrack.js +54 -0
  9. package/dist/components/ConservationTrack.js.map +1 -0
  10. package/dist/components/Loading.js +14 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +36 -0
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/SequenceTextArea.js +3 -2
  15. package/dist/components/SequenceTextArea.js.map +1 -1
  16. package/dist/components/TextTrack.d.ts +3 -3
  17. package/dist/components/TextTrack.js +4 -1
  18. package/dist/components/TextTrack.js.map +1 -1
  19. package/dist/components/Track.js +21 -8
  20. package/dist/components/Track.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +19 -3
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/header/GappynessSlider.d.ts +6 -0
  24. package/dist/components/header/GappynessSlider.js +19 -0
  25. package/dist/components/header/GappynessSlider.js.map +1 -0
  26. package/dist/components/header/Header.js +3 -1
  27. package/dist/components/header/Header.js.map +1 -1
  28. package/dist/components/header/HeaderMenu.js +30 -14
  29. package/dist/components/header/HeaderMenu.js.map +1 -1
  30. package/dist/components/minimap/MinimapSVG.js +4 -3
  31. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  32. package/dist/components/msa/MSACanvasBlock.js +56 -42
  33. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  34. package/dist/components/msa/renderMSABlock.js +53 -10
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  37. package/dist/components/tracks/renderTracksSvg.js +83 -0
  38. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  39. package/dist/components/tree/TreeNodeMenu.js +2 -2
  40. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  41. package/dist/components/tree/renderTreeCanvas.js +1 -1
  42. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  43. package/dist/constants.d.ts +22 -0
  44. package/dist/constants.js +26 -0
  45. package/dist/constants.js.map +1 -0
  46. package/dist/layout.js.map +1 -1
  47. package/dist/model/msaModel.js +3 -2
  48. package/dist/model/msaModel.js.map +1 -1
  49. package/dist/model/treeModel.js +9 -8
  50. package/dist/model/treeModel.js.map +1 -1
  51. package/dist/model.d.ts +256 -15
  52. package/dist/model.js +408 -128
  53. package/dist/model.js.map +1 -1
  54. package/dist/neighborJoining.d.ts +1 -0
  55. package/dist/neighborJoining.js +839 -0
  56. package/dist/neighborJoining.js.map +1 -0
  57. package/dist/neighborJoining.test.d.ts +1 -0
  58. package/dist/neighborJoining.test.js +110 -0
  59. package/dist/neighborJoining.test.js.map +1 -0
  60. package/dist/parsers/A3mMSA.d.ts +43 -0
  61. package/dist/parsers/A3mMSA.js +277 -0
  62. package/dist/parsers/A3mMSA.js.map +1 -0
  63. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  64. package/dist/parsers/A3mMSA.test.js +138 -0
  65. package/dist/parsers/A3mMSA.test.js.map +1 -0
  66. package/dist/parsers/ClustalMSA.d.ts +4 -4
  67. package/dist/parsers/ClustalMSA.js +3 -1
  68. package/dist/parsers/ClustalMSA.js.map +1 -1
  69. package/dist/parsers/FastaMSA.js +17 -16
  70. package/dist/parsers/FastaMSA.js.map +1 -1
  71. package/dist/renderToSvg.d.ts +1 -0
  72. package/dist/renderToSvg.js +48 -18
  73. package/dist/renderToSvg.js.map +1 -1
  74. package/dist/rowCoordinateCalculations.js +2 -0
  75. package/dist/rowCoordinateCalculations.js.map +1 -1
  76. package/dist/types.d.ts +2 -3
  77. package/dist/util.js +17 -9
  78. package/dist/util.js.map +1 -1
  79. package/dist/version.d.ts +1 -1
  80. package/dist/version.js +1 -1
  81. package/package.json +6 -6
  82. package/src/colorSchemes.ts +1 -179
  83. package/src/components/ConservationTrack.tsx +104 -0
  84. package/src/components/Loading.tsx +44 -2
  85. package/src/components/MSAView.tsx +68 -0
  86. package/src/components/SequenceTextArea.tsx +3 -2
  87. package/src/components/TextTrack.tsx +7 -4
  88. package/src/components/Track.tsx +25 -9
  89. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  90. package/src/components/header/GappynessSlider.tsx +35 -0
  91. package/src/components/header/Header.tsx +3 -1
  92. package/src/components/header/HeaderMenu.tsx +36 -15
  93. package/src/components/minimap/MinimapSVG.tsx +6 -3
  94. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  95. package/src/components/msa/renderMSABlock.ts +82 -22
  96. package/src/components/tracks/renderTracksSvg.ts +157 -0
  97. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  98. package/src/components/tree/renderTreeCanvas.ts +1 -1
  99. package/src/constants.ts +27 -0
  100. package/src/layout.ts +1 -6
  101. package/src/model/msaModel.ts +4 -2
  102. package/src/model/treeModel.ts +19 -8
  103. package/src/model.ts +496 -140
  104. package/src/neighborJoining.test.ts +129 -0
  105. package/src/neighborJoining.ts +885 -0
  106. package/src/parsers/A3mMSA.test.ts +164 -0
  107. package/src/parsers/A3mMSA.ts +321 -0
  108. package/src/parsers/ClustalMSA.ts +7 -5
  109. package/src/parsers/FastaMSA.ts +17 -17
  110. package/src/renderToSvg.tsx +105 -26
  111. package/src/rowCoordinateCalculations.ts +2 -0
  112. package/src/types.ts +2 -4
  113. package/src/util.ts +21 -8
  114. package/src/version.ts +1 -1
  115. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  116. package/dist/components/dialogs/TracklistDialog.js +0 -23
  117. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  118. package/src/components/dialogs/TracklistDialog.tsx +0 -73
package/dist/model.js CHANGED
@@ -11,7 +11,9 @@ import { addDisposer, cast, types } from 'mobx-state-tree';
11
11
  import Stockholm from 'stockholm-js';
12
12
  import { blocksX, blocksY } from './calculateBlocks';
13
13
  import colorSchemes from './colorSchemes';
14
+ import ConservationTrack from './components/ConservationTrack';
14
15
  import TextTrack from './components/TextTrack';
16
+ import { defaultAllowedGappyness, defaultBgColor, defaultColWidth, defaultColorSchemeName, defaultContrastLettering, defaultCurrentAlignment, defaultDrawLabels, defaultDrawMsaLetters, defaultDrawNodeBubbles, defaultDrawTree, defaultHeight, defaultHideGaps, defaultLabelsAlignRight, defaultRowHeight, defaultScrollX, defaultScrollY, defaultShowBranchLen, defaultShowDomains, defaultSubFeatureRows, defaultTreeAreaWidth, defaultTreeWidth, defaultTreeWidthMatchesArea, } from './constants';
15
17
  import { flatToTree } from './flatToTree';
16
18
  import palettes from './ggplotPalettes';
17
19
  import { measureTextCanvas } from './measureTextCanvas';
@@ -19,8 +21,10 @@ import { DataModelF } from './model/DataModel';
19
21
  import { DialogQueueSessionMixin } from './model/DialogQueue';
20
22
  import { MSAModelF } from './model/msaModel';
21
23
  import { TreeModelF } from './model/treeModel';
24
+ import { calculateNeighborJoiningTree } from './neighborJoining';
22
25
  import { parseAsn1 } from './parseAsn1';
23
26
  import parseNewick from './parseNewick';
27
+ import A3mMSA from './parsers/A3mMSA';
24
28
  import ClustalMSA from './parsers/ClustalMSA';
25
29
  import EmfMSA from './parsers/EmfMSA';
26
30
  import FastaMSA from './parsers/FastaMSA';
@@ -28,9 +32,7 @@ import StockholmMSA from './parsers/StockholmMSA';
28
32
  import { reparseTree } from './reparseTree';
29
33
  import { mouseOverCoordToGapRemovedRowCoord, mouseOverCoordToGlobalCoord, } from './rowCoordinateCalculations';
30
34
  import { seqCoordToRowSpecificGlobalCoord } from './seqCoordToRowSpecificGlobalCoord';
31
- import { collapse, generateNodeIds, isBlank, len, maxLength, setBrLength, skipBlanks, } from './util';
32
- const defaultRowHeight = 16;
33
- const defaultColWidth = 12;
35
+ import { collapse, generateNodeIds, len, maxLength, setBrLength, skipBlanks, } from './util';
34
36
  const showZoomStarKey = 'msa-showZoomStar';
35
37
  /**
36
38
  * #stateModel MsaView
@@ -50,23 +52,23 @@ function stateModelFactory() {
50
52
  /**
51
53
  * #property
52
54
  */
53
- showDomains: false,
55
+ showDomains: defaultShowDomains,
54
56
  /**
55
57
  * #property
56
58
  */
57
- hideGaps: true,
59
+ hideGaps: defaultHideGaps,
58
60
  /**
59
61
  * #property
60
62
  */
61
- allowedGappyness: 100,
63
+ allowedGappyness: defaultAllowedGappyness,
62
64
  /**
63
65
  * #property
64
66
  */
65
- contrastLettering: true,
67
+ contrastLettering: defaultContrastLettering,
66
68
  /**
67
69
  * #property
68
70
  */
69
- subFeatureRows: false,
71
+ subFeatureRows: defaultSubFeatureRows,
70
72
  /**
71
73
  * #property
72
74
  * hardcoded view type
@@ -75,12 +77,12 @@ function stateModelFactory() {
75
77
  /**
76
78
  * #property
77
79
  */
78
- drawMsaLetters: true,
80
+ drawMsaLetters: defaultDrawMsaLetters,
79
81
  /**
80
82
  * #property
81
83
  * height of the div containing the view, px
82
84
  */
83
- height: types.optional(types.number, 550),
85
+ height: types.optional(types.number, defaultHeight),
84
86
  /**
85
87
  * #property
86
88
  * height of each row, px
@@ -90,12 +92,12 @@ function stateModelFactory() {
90
92
  * #property
91
93
  * scroll position, Y-offset, px
92
94
  */
93
- scrollY: 0,
95
+ scrollY: defaultScrollY,
94
96
  /**
95
97
  * #property
96
98
  * scroll position, X-offset, px
97
99
  */
98
- scrollX: 0,
100
+ scrollX: defaultScrollX,
99
101
  /**
100
102
  * #property
101
103
  * width of columns, px
@@ -121,7 +123,7 @@ function stateModelFactory() {
121
123
  * #property
122
124
  *
123
125
  */
124
- currentAlignment: 0,
126
+ currentAlignment: defaultCurrentAlignment,
125
127
  /**
126
128
  * #property
127
129
  * array of tree parent nodes that are 'collapsed' (all children are
@@ -289,7 +291,7 @@ function stateModelFactory() {
289
291
  self.loadingMSA = arg;
290
292
  },
291
293
  /**
292
- * #volatile
294
+ * #action
293
295
  */
294
296
  setShowZoomStar(arg) {
295
297
  self.showZoomStar = arg;
@@ -338,7 +340,7 @@ function stateModelFactory() {
338
340
  return;
339
341
  }
340
342
  // Find the node in the hierarchy
341
- const node = self.hierarchy.find((n) => n.data.id === nodeId);
343
+ const node = self.hierarchy.find(n => n.data.id === nodeId);
342
344
  if (!node) {
343
345
  self.hoveredTreeNode = undefined;
344
346
  return;
@@ -409,7 +411,7 @@ function stateModelFactory() {
409
411
  /**
410
412
  * #action
411
413
  */
412
- toggleCollapsed2(node) {
414
+ toggleCollapsedLeaf(node) {
413
415
  if (self.collapsedLeaves.includes(node)) {
414
416
  self.collapsedLeaves.remove(node);
415
417
  }
@@ -461,11 +463,21 @@ function stateModelFactory() {
461
463
  },
462
464
  }))
463
465
  .views(self => ({
466
+ /**
467
+ * #getter
468
+ * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
469
+ */
470
+ get hideGapsEffective() {
471
+ return (self.hideGaps &&
472
+ (self.collapsed.length > 0 ||
473
+ self.collapsedLeaves.length > 0 ||
474
+ self.allowedGappyness < 100));
475
+ },
464
476
  /**
465
477
  * #getter
466
478
  */
467
479
  get realAllowedGappyness() {
468
- return self.hideGaps ? self.allowedGappyness : 100;
480
+ return this.hideGapsEffective ? self.allowedGappyness : 100;
469
481
  },
470
482
  /**
471
483
  * #getter
@@ -548,6 +560,9 @@ function stateModelFactory() {
548
560
  if (Stockholm.sniff(text)) {
549
561
  return new StockholmMSA(text, self.currentAlignment);
550
562
  }
563
+ else if (A3mMSA.sniff(text)) {
564
+ return new A3mMSA(text);
565
+ }
551
566
  else if (text.startsWith('>')) {
552
567
  return new FastaMSA(text);
553
568
  }
@@ -595,6 +610,33 @@ function stateModelFactory() {
595
610
  const { mouseRow } = self;
596
611
  return mouseRow === undefined ? undefined : this.rowNames[mouseRow];
597
612
  },
613
+ /**
614
+ * #getter
615
+ * Returns insertion info if mouse is hovering over an insertion indicator
616
+ */
617
+ get hoveredInsertion() {
618
+ const { mouseCol, mouseRow } = self;
619
+ if (mouseCol === undefined || mouseRow === undefined) {
620
+ return undefined;
621
+ }
622
+ const rowName = this.rowNames[mouseRow];
623
+ if (!rowName) {
624
+ return undefined;
625
+ }
626
+ const insertions = this.insertionPositions.get(rowName);
627
+ if (!insertions) {
628
+ return undefined;
629
+ }
630
+ const insertion = insertions.find(ins => ins.pos === mouseCol);
631
+ if (insertion) {
632
+ return {
633
+ rowName,
634
+ col: mouseCol,
635
+ letters: insertion.letters,
636
+ };
637
+ }
638
+ return undefined;
639
+ },
598
640
  /**
599
641
  * #getter
600
642
  */
@@ -615,7 +657,7 @@ function stateModelFactory() {
615
657
  [...self.collapsed, ...self.collapsedLeaves]
616
658
  .map(collapsedId => hier.find(node => node.data.id === collapsedId))
617
659
  .filter(notEmpty)
618
- .map(node => {
660
+ .forEach(node => {
619
661
  collapse(node);
620
662
  });
621
663
  return hier;
@@ -637,28 +679,35 @@ function stateModelFactory() {
637
679
  * #getter
638
680
  */
639
681
  get blanks() {
640
- const { hideGaps, realAllowedGappyness } = self;
641
- const blanks = [];
642
- if (hideGaps) {
643
- const strs = this.leaves
644
- .map(leaf => this.MSA?.getRow(leaf.data.name))
645
- .filter(notEmpty);
646
- if (strs.length) {
647
- const s0len = strs[0].length;
648
- for (let i = 0; i < s0len; i++) {
649
- let counter = 0;
650
- const l = strs.length;
651
- for (let j = 0; j < l; j++) {
652
- if (isBlank(strs[j][i])) {
653
- counter++;
654
- }
655
- }
656
- if (counter / l >= realAllowedGappyness / 100) {
657
- blanks.push(i);
658
- }
682
+ const { hideGapsEffective, realAllowedGappyness } = self;
683
+ if (!hideGapsEffective) {
684
+ return [];
685
+ }
686
+ const strs = this.leaves
687
+ .map(leaf => this.MSA?.getRow(leaf.data.name))
688
+ .filter(notEmpty);
689
+ if (strs.length === 0) {
690
+ return [];
691
+ }
692
+ const numCols = strs[0].length;
693
+ const numRows = strs.length;
694
+ const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
695
+ const blankCounts = new Uint16Array(numCols);
696
+ for (let j = 0; j < numRows; j++) {
697
+ const str = strs[j];
698
+ for (let i = 0; i < numCols; i++) {
699
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
700
+ if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
701
+ blankCounts[i]++;
659
702
  }
660
703
  }
661
704
  }
705
+ const blanks = [];
706
+ for (let i = 0; i < numCols; i++) {
707
+ if (blankCounts[i] >= threshold) {
708
+ blanks.push(i);
709
+ }
710
+ }
662
711
  return blanks;
663
712
  },
664
713
  /**
@@ -667,6 +716,62 @@ function stateModelFactory() {
667
716
  get blanksSet() {
668
717
  return new Set(this.blanks);
669
718
  },
719
+ /**
720
+ * #getter
721
+ * Returns a map of row name to array of insertions with display position and letters
722
+ */
723
+ get insertionPositions() {
724
+ const { hideGapsEffective } = self;
725
+ const { blanks, rows } = this;
726
+ const blanksLen = blanks.length;
727
+ if (blanksLen === 0 || !hideGapsEffective) {
728
+ return new Map();
729
+ }
730
+ const result = new Map();
731
+ for (const [name, seq] of rows) {
732
+ const insertions = [];
733
+ let displayPos = 0;
734
+ let blankIdx = 0;
735
+ let currentInsertPos = -1;
736
+ let letterChars = [];
737
+ const seqLen = seq.length;
738
+ for (let i = 0; i < seqLen; i++) {
739
+ if (blankIdx < blanksLen && blanks[blankIdx] === i) {
740
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
741
+ const code = seq.charCodeAt(i);
742
+ if (!((code - 45) >>> 0 <= 1)) {
743
+ if (currentInsertPos === displayPos) {
744
+ letterChars.push(seq[i]);
745
+ }
746
+ else {
747
+ if (letterChars.length > 0) {
748
+ insertions.push({
749
+ pos: currentInsertPos,
750
+ letters: letterChars.join(''),
751
+ });
752
+ }
753
+ currentInsertPos = displayPos;
754
+ letterChars = [seq[i]];
755
+ }
756
+ }
757
+ blankIdx++;
758
+ }
759
+ else {
760
+ displayPos++;
761
+ }
762
+ }
763
+ if (letterChars.length > 0) {
764
+ insertions.push({
765
+ pos: currentInsertPos,
766
+ letters: letterChars.join(''),
767
+ });
768
+ }
769
+ if (insertions.length > 0) {
770
+ result.set(name, insertions);
771
+ }
772
+ }
773
+ return result;
774
+ },
670
775
  /**
671
776
  * #getter
672
777
  */
@@ -698,10 +803,10 @@ function stateModelFactory() {
698
803
  * #getter
699
804
  */
700
805
  get columns2d() {
701
- const { hideGaps } = self;
806
+ const { hideGapsEffective } = self;
702
807
  return this.rows
703
808
  .map(r => r[1])
704
- .map(str => (hideGaps ? skipBlanks(this.blanks, str) : str));
809
+ .map(str => (hideGapsEffective ? skipBlanks(this.blanks, str) : str));
705
810
  },
706
811
  /**
707
812
  * #getter
@@ -734,6 +839,182 @@ function stateModelFactory() {
734
839
  get colStatsSums() {
735
840
  return this.colStats.map(val => sum(Object.values(val)));
736
841
  },
842
+ /**
843
+ * #getter
844
+ * Pre-computed consensus letter and percent identity color per column.
845
+ * Used by percent_identity_dynamic color scheme.
846
+ */
847
+ get colConsensus() {
848
+ const { colStats, colStatsSums } = this;
849
+ return colStats.map((stats, i) => {
850
+ const total = colStatsSums[i];
851
+ let maxCount = 0;
852
+ let letter = '';
853
+ for (const key in stats) {
854
+ const val = stats[key];
855
+ if (val > maxCount && key !== '-' && key !== '.') {
856
+ maxCount = val;
857
+ letter = key;
858
+ }
859
+ }
860
+ const proportion = maxCount / total;
861
+ return {
862
+ letter,
863
+ color: proportion > 0.4
864
+ ? `hsl(240, 30%, ${100 * Math.max(1 - proportion / 3, 0.3)}%)`
865
+ : undefined,
866
+ };
867
+ });
868
+ },
869
+ /**
870
+ * #getter
871
+ * Pre-computed ClustalX colors per column.
872
+ * Returns a map of letter -> color for each column.
873
+ * ref http://www.jalview.org/help/html/colourSchemes/clustal.html
874
+ */
875
+ get colClustalX() {
876
+ const { colStats, colStatsSums } = this;
877
+ return colStats.map((stats, i) => {
878
+ const total = colStatsSums[i];
879
+ const colors = {};
880
+ const W = stats.W ?? 0;
881
+ const L = stats.L ?? 0;
882
+ const V = stats.V ?? 0;
883
+ const I = stats.I ?? 0;
884
+ const M = stats.M ?? 0;
885
+ const A = stats.A ?? 0;
886
+ const F = stats.F ?? 0;
887
+ const C = stats.C ?? 0;
888
+ const H = stats.H ?? 0;
889
+ const P = stats.P ?? 0;
890
+ const R = stats.R ?? 0;
891
+ const K = stats.K ?? 0;
892
+ const Q = stats.Q ?? 0;
893
+ const E = stats.E ?? 0;
894
+ const D = stats.D ?? 0;
895
+ const T = stats.T ?? 0;
896
+ const S = stats.S ?? 0;
897
+ const G = stats.G ?? 0;
898
+ const Y = stats.Y ?? 0;
899
+ const N = stats.N ?? 0;
900
+ const WLVIMAFCHPY = W + L + V + I + M + A + F + C + H + P + Y;
901
+ const KR = K + R;
902
+ const QE = Q + E;
903
+ const ED = E + D;
904
+ const TS = T + S;
905
+ if (WLVIMAFCHPY / total > 0.6) {
906
+ colors.W = 'rgb(128,179,230)';
907
+ colors.L = 'rgb(128,179,230)';
908
+ colors.V = 'rgb(128,179,230)';
909
+ colors.A = 'rgb(128,179,230)';
910
+ colors.I = 'rgb(128,179,230)';
911
+ colors.M = 'rgb(128,179,230)';
912
+ colors.F = 'rgb(128,179,230)';
913
+ colors.C = 'rgb(128,179,230)';
914
+ }
915
+ if (KR / total > 0.6 ||
916
+ K / total > 0.8 ||
917
+ R / total > 0.8 ||
918
+ Q / total > 0.8) {
919
+ colors.K = '#d88';
920
+ colors.R = '#d88';
921
+ }
922
+ if (KR / total > 0.6 ||
923
+ QE / total > 0.5 ||
924
+ E / total > 0.8 ||
925
+ Q / total > 0.8 ||
926
+ D / total > 0.8) {
927
+ colors.E = 'rgb(192, 72, 192)';
928
+ }
929
+ if (KR / total > 0.6 ||
930
+ ED / total > 0.5 ||
931
+ K / total > 0.8 ||
932
+ R / total > 0.8 ||
933
+ Q / total > 0.8) {
934
+ colors.D = 'rgb(204, 77, 204)';
935
+ }
936
+ if (N / total > 0.5 || Y / total > 0.85) {
937
+ colors.N = '#8f8';
938
+ }
939
+ if (KR / total > 0.6 ||
940
+ QE / total > 0.6 ||
941
+ Q / total > 0.85 ||
942
+ E / total > 0.85 ||
943
+ K / total > 0.85 ||
944
+ R / total > 0.85) {
945
+ colors.Q = '#8f8';
946
+ }
947
+ if (WLVIMAFCHPY / total > 0.6 ||
948
+ TS / total > 0.5 ||
949
+ S / total > 0.85 ||
950
+ T / total > 0.85) {
951
+ colors.S = 'rgb(26,204,26)';
952
+ colors.T = 'rgb(26,204,26)';
953
+ }
954
+ if (C / total > 0.85) {
955
+ colors.C = 'rgb(240, 128, 128)';
956
+ }
957
+ if (G / total > 0) {
958
+ colors.G = 'rgb(240, 144, 72)';
959
+ }
960
+ if (P / total > 0) {
961
+ colors.P = 'rgb(204, 204, 0)';
962
+ }
963
+ if (WLVIMAFCHPY / total > 0.6 ||
964
+ W / total > 0.85 ||
965
+ Y / total > 0.85 ||
966
+ A / total > 0.85 ||
967
+ C / total > 0.85 ||
968
+ P / total > 0.85 ||
969
+ Q / total > 0.85 ||
970
+ F / total > 0.85 ||
971
+ H / total > 0.85 ||
972
+ I / total > 0.85 ||
973
+ L / total > 0.85 ||
974
+ M / total > 0.85 ||
975
+ V / total > 0.85) {
976
+ colors.H = 'rgb(26, 179, 179)';
977
+ colors.Y = 'rgb(26, 179, 179)';
978
+ }
979
+ return colors;
980
+ });
981
+ },
982
+ /**
983
+ * #getter
984
+ * Conservation score per column using Shannon entropy (biojs-msa style).
985
+ * Conservation = (1 - H/Hmax) * (1 - gapFraction)
986
+ * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
987
+ */
988
+ get conservation() {
989
+ const { colStats, colStatsSums } = this;
990
+ const alphabetSize = 20;
991
+ const maxEntropy = Math.log2(alphabetSize);
992
+ return colStats.map((stats, i) => {
993
+ const total = colStatsSums[i];
994
+ if (!total) {
995
+ return 0;
996
+ }
997
+ const gapCount = (stats['-'] || 0) + (stats['.'] || 0);
998
+ const nonGapTotal = total - gapCount;
999
+ if (nonGapTotal === 0) {
1000
+ return 0;
1001
+ }
1002
+ let entropy = 0;
1003
+ for (const letter of Object.keys(stats)) {
1004
+ if (letter === '-' || letter === '.') {
1005
+ continue;
1006
+ }
1007
+ const count = stats[letter];
1008
+ const freq = count / nonGapTotal;
1009
+ if (freq > 0) {
1010
+ entropy -= freq * Math.log2(freq);
1011
+ }
1012
+ }
1013
+ const gapFraction = gapCount / total;
1014
+ const conservation = Math.max(0, 1 - entropy / maxEntropy);
1015
+ return conservation * (1 - gapFraction);
1016
+ });
1017
+ },
737
1018
  /**
738
1019
  * #getter
739
1020
  * generates a new tree that is clustered with x,y positions
@@ -851,6 +1132,17 @@ function stateModelFactory() {
851
1132
  setDrawMsaLetters(arg) {
852
1133
  self.drawMsaLetters = arg;
853
1134
  },
1135
+ /**
1136
+ * #action
1137
+ * Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
1138
+ */
1139
+ calculateNeighborJoiningTreeFromMSA() {
1140
+ if (self.rows.length < 2) {
1141
+ throw new Error('Need at least 2 sequences to build a tree');
1142
+ }
1143
+ const newickTree = calculateNeighborJoiningTree(self.rows);
1144
+ self.setTree(newickTree);
1145
+ },
854
1146
  /**
855
1147
  * #action
856
1148
  */
@@ -972,33 +1264,17 @@ function stateModelFactory() {
972
1264
  get seqConsensus() {
973
1265
  return self.MSA?.seqConsensus;
974
1266
  },
975
- /**
976
- * #getter
977
- */
978
- get conservation() {
979
- if (self.columns2d.length) {
980
- for (let i = 0; i < self.columns2d[0].length; i++) {
981
- const col = [];
982
- for (const column of self.columns2d) {
983
- col.push(column[i]);
984
- }
985
- }
986
- }
987
- return ['a'];
988
- },
989
1267
  /**
990
1268
  * #getter
991
1269
  */
992
1270
  get adapterTrackModels() {
993
- const { rowHeight, MSA, hideGaps, blanks } = self;
994
- return (MSA?.tracks.map(t => ({
1271
+ const { rowHeight, MSA, hideGapsEffective, blanks } = self;
1272
+ return (MSA?.tracks
1273
+ .filter(t => t.data)
1274
+ .map(t => ({
995
1275
  model: {
996
1276
  ...t,
997
- data: t.data
998
- ? hideGaps
999
- ? skipBlanks(blanks, t.data)
1000
- : t.data
1001
- : undefined,
1277
+ data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
1002
1278
  height: rowHeight,
1003
1279
  },
1004
1280
  ReactComponent: TextTrack,
@@ -1008,7 +1284,15 @@ function stateModelFactory() {
1008
1284
  * #getter
1009
1285
  */
1010
1286
  get tracks() {
1011
- return this.adapterTrackModels;
1287
+ const conservationTrack = {
1288
+ model: {
1289
+ id: 'conservation',
1290
+ name: 'Conservation',
1291
+ height: 40,
1292
+ },
1293
+ ReactComponent: ConservationTrack,
1294
+ };
1295
+ return [...this.adapterTrackModels, conservationTrack];
1012
1296
  },
1013
1297
  /**
1014
1298
  * #getter
@@ -1359,81 +1643,77 @@ function stateModelFactory() {
1359
1643
  }))
1360
1644
  .postProcessSnapshot(result => {
1361
1645
  const snap = result;
1362
- const { data: { tree, msa, treeMetadata }, ...rest } = snap;
1363
- // Default values to filter out
1364
- const defaults = {
1365
- // Main model defaults
1366
- showDomains: false,
1367
- hideGaps: true,
1368
- allowedGappyness: 100,
1369
- contrastLettering: true,
1370
- subFeatureRows: false,
1371
- drawMsaLetters: true,
1372
- height: 550,
1373
- rowHeight: defaultRowHeight,
1374
- scrollY: 0,
1375
- scrollX: 0,
1376
- colWidth: defaultColWidth,
1377
- currentAlignment: 0,
1378
- // MSA model defaults
1379
- bgColor: true,
1380
- colorSchemeName: 'maeditor',
1381
- // Tree model defaults
1382
- drawLabels: true,
1383
- labelsAlignRight: false,
1384
- treeAreaWidth: 400,
1385
- treeWidth: 300,
1386
- treeWidthMatchesArea: true,
1387
- showBranchLen: true,
1388
- drawTree: true,
1389
- drawNodeBubbles: true,
1390
- };
1391
- // Properties that should always be included even if they match defaults
1392
- const alwaysInclude = new Set(['id', 'type', 'relativeTo']);
1393
- // Filter out properties that match default values
1394
- function filterDefaults(obj) {
1395
- const filtered = {};
1396
- for (const [key, value] of Object.entries(obj)) {
1397
- // Always include essential properties
1398
- if (alwaysInclude.has(key)) {
1399
- filtered[key] = value;
1400
- continue;
1401
- }
1402
- // Skip if value matches default
1403
- if (defaults[key] === value) {
1404
- continue;
1405
- }
1406
- // Handle nested objects
1407
- if (value && typeof value === 'object' && !Array.isArray(value)) {
1408
- const filteredNested = filterDefaults(value);
1409
- // Only include nested object if it has non-default properties
1410
- if (Object.keys(filteredNested).length > 0) {
1411
- filtered[key] = filteredNested;
1412
- }
1413
- }
1414
- else if (Array.isArray(value)) {
1415
- // Only include arrays that aren't empty
1416
- if (value.length > 0) {
1417
- filtered[key] = value;
1418
- }
1419
- }
1420
- else {
1421
- // Include non-default primitives
1422
- filtered[key] = value;
1423
- }
1424
- }
1425
- return filtered;
1426
- }
1427
- const filteredRest = filterDefaults(rest);
1646
+ const { data: { tree, msa, treeMetadata },
1647
+ // Main model properties
1648
+ showDomains, hideGaps, allowedGappyness, contrastLettering, subFeatureRows, drawMsaLetters, height, rowHeight, scrollY, scrollX, colWidth, currentAlignment, collapsed, collapsedLeaves, showOnly, turnedOffTracks, featureFilters, relativeTo,
1649
+ // MSA model properties
1650
+ bgColor, colorSchemeName,
1651
+ // Tree model properties
1652
+ drawLabels, labelsAlignRight, treeAreaWidth, treeWidth, treeWidthMatchesArea, showBranchLen, drawTree, drawNodeBubbles,
1653
+ // Always include
1654
+ ...rest } = snap;
1428
1655
  // remove the MSA/tree data from the tree if the filehandle available in
1429
1656
  // which case it can be reloaded on refresh
1430
1657
  return {
1658
+ ...rest,
1431
1659
  data: {
1432
1660
  ...(result.treeFilehandle ? {} : { tree }),
1433
1661
  ...(result.msaFilehandle ? {} : { msa }),
1434
1662
  ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
1435
1663
  },
1436
- ...filteredRest,
1664
+ // Main model - only include non-default values
1665
+ ...(showDomains !== defaultShowDomains ? { showDomains } : {}),
1666
+ ...(hideGaps !== defaultHideGaps ? { hideGaps } : {}),
1667
+ ...(allowedGappyness !== defaultAllowedGappyness
1668
+ ? { allowedGappyness }
1669
+ : {}),
1670
+ ...(contrastLettering !== defaultContrastLettering
1671
+ ? { contrastLettering }
1672
+ : {}),
1673
+ ...(subFeatureRows !== defaultSubFeatureRows ? { subFeatureRows } : {}),
1674
+ ...(drawMsaLetters !== defaultDrawMsaLetters ? { drawMsaLetters } : {}),
1675
+ ...(height !== defaultHeight ? { height } : {}),
1676
+ ...(rowHeight !== defaultRowHeight ? { rowHeight } : {}),
1677
+ ...(scrollY !== defaultScrollY ? { scrollY } : {}),
1678
+ ...(scrollX !== defaultScrollX ? { scrollX } : {}),
1679
+ ...(colWidth !== defaultColWidth ? { colWidth } : {}),
1680
+ ...(currentAlignment !== defaultCurrentAlignment
1681
+ ? { currentAlignment }
1682
+ : {}),
1683
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1684
+ ...(collapsed?.length ? { collapsed } : {}),
1685
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1686
+ ...(collapsedLeaves?.length ? { collapsedLeaves } : {}),
1687
+ ...(showOnly !== undefined ? { showOnly } : {}),
1688
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1689
+ ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
1690
+ ? { turnedOffTracks }
1691
+ : {}),
1692
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1693
+ ...(featureFilters && Object.keys(featureFilters).length > 0
1694
+ ? { featureFilters }
1695
+ : {}),
1696
+ ...(relativeTo !== undefined ? { relativeTo } : {}),
1697
+ // MSA model - only include non-default values
1698
+ ...(bgColor !== defaultBgColor ? { bgColor } : {}),
1699
+ ...(colorSchemeName !== defaultColorSchemeName
1700
+ ? { colorSchemeName }
1701
+ : {}),
1702
+ // Tree model - only include non-default values
1703
+ ...(drawLabels !== defaultDrawLabels ? { drawLabels } : {}),
1704
+ ...(labelsAlignRight !== defaultLabelsAlignRight
1705
+ ? { labelsAlignRight }
1706
+ : {}),
1707
+ ...(treeAreaWidth !== defaultTreeAreaWidth ? { treeAreaWidth } : {}),
1708
+ ...(treeWidth !== defaultTreeWidth ? { treeWidth } : {}),
1709
+ ...(treeWidthMatchesArea !== defaultTreeWidthMatchesArea
1710
+ ? { treeWidthMatchesArea }
1711
+ : {}),
1712
+ ...(showBranchLen !== defaultShowBranchLen ? { showBranchLen } : {}),
1713
+ ...(drawTree !== defaultDrawTree ? { drawTree } : {}),
1714
+ ...(drawNodeBubbles !== defaultDrawNodeBubbles
1715
+ ? { drawNodeBubbles }
1716
+ : {}),
1437
1717
  };
1438
1718
  });
1439
1719
  }