react-msaview 4.4.6 → 4.6.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 (122) 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 +71 -26
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/msa/renderMSAMouseover.js +8 -1
  37. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  38. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  39. package/dist/components/tracks/renderTracksSvg.js +83 -0
  40. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  41. package/dist/components/tree/TreeNodeMenu.js +2 -2
  42. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  43. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  44. package/dist/components/tree/renderTreeCanvas.js +23 -24
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +22 -0
  47. package/dist/constants.js +26 -0
  48. package/dist/constants.js.map +1 -0
  49. package/dist/layout.js.map +1 -1
  50. package/dist/model/msaModel.js +3 -2
  51. package/dist/model/msaModel.js.map +1 -1
  52. package/dist/model/treeModel.js +9 -8
  53. package/dist/model/treeModel.js.map +1 -1
  54. package/dist/model.d.ts +271 -15
  55. package/dist/model.js +427 -128
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.d.ts +1 -0
  58. package/dist/neighborJoining.js +839 -0
  59. package/dist/neighborJoining.js.map +1 -0
  60. package/dist/neighborJoining.test.d.ts +1 -0
  61. package/dist/neighborJoining.test.js +110 -0
  62. package/dist/neighborJoining.test.js.map +1 -0
  63. package/dist/parsers/A3mMSA.d.ts +43 -0
  64. package/dist/parsers/A3mMSA.js +277 -0
  65. package/dist/parsers/A3mMSA.js.map +1 -0
  66. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  67. package/dist/parsers/A3mMSA.test.js +138 -0
  68. package/dist/parsers/A3mMSA.test.js.map +1 -0
  69. package/dist/parsers/ClustalMSA.d.ts +4 -4
  70. package/dist/parsers/ClustalMSA.js +3 -1
  71. package/dist/parsers/ClustalMSA.js.map +1 -1
  72. package/dist/parsers/FastaMSA.js +17 -16
  73. package/dist/parsers/FastaMSA.js.map +1 -1
  74. package/dist/renderToSvg.d.ts +1 -0
  75. package/dist/renderToSvg.js +48 -18
  76. package/dist/renderToSvg.js.map +1 -1
  77. package/dist/rowCoordinateCalculations.js +2 -0
  78. package/dist/rowCoordinateCalculations.js.map +1 -1
  79. package/dist/types.d.ts +2 -3
  80. package/dist/util.js +17 -9
  81. package/dist/util.js.map +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +6 -6
  85. package/src/colorSchemes.ts +1 -179
  86. package/src/components/ConservationTrack.tsx +104 -0
  87. package/src/components/Loading.tsx +44 -2
  88. package/src/components/MSAView.tsx +68 -0
  89. package/src/components/SequenceTextArea.tsx +3 -2
  90. package/src/components/TextTrack.tsx +7 -4
  91. package/src/components/Track.tsx +25 -9
  92. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  93. package/src/components/header/GappynessSlider.tsx +35 -0
  94. package/src/components/header/Header.tsx +3 -1
  95. package/src/components/header/HeaderMenu.tsx +36 -15
  96. package/src/components/minimap/MinimapSVG.tsx +6 -3
  97. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  98. package/src/components/msa/renderMSABlock.ts +103 -40
  99. package/src/components/msa/renderMSAMouseover.ts +9 -0
  100. package/src/components/tracks/renderTracksSvg.ts +157 -0
  101. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  102. package/src/components/tree/renderTreeCanvas.ts +25 -34
  103. package/src/constants.ts +27 -0
  104. package/src/layout.ts +1 -6
  105. package/src/model/msaModel.ts +4 -2
  106. package/src/model/treeModel.ts +19 -8
  107. package/src/model.ts +517 -140
  108. package/src/neighborJoining.test.ts +129 -0
  109. package/src/neighborJoining.ts +885 -0
  110. package/src/parsers/A3mMSA.test.ts +164 -0
  111. package/src/parsers/A3mMSA.ts +321 -0
  112. package/src/parsers/ClustalMSA.ts +7 -5
  113. package/src/parsers/FastaMSA.ts +17 -17
  114. package/src/renderToSvg.tsx +105 -26
  115. package/src/rowCoordinateCalculations.ts +2 -0
  116. package/src/types.ts +2 -4
  117. package/src/util.ts +21 -8
  118. package/src/version.ts +1 -1
  119. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  120. package/dist/components/dialogs/TracklistDialog.js +0 -23
  121. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  122. 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
@@ -230,6 +232,11 @@ function stateModelFactory() {
230
232
  * the currently hovered tree node ID and its descendant leaf names
231
233
  */
232
234
  hoveredTreeNode: undefined,
235
+ /**
236
+ * #volatile
237
+ * array of column indices to highlight
238
+ */
239
+ highlightedColumns: undefined,
233
240
  /**
234
241
  * #volatile
235
242
  * a dummy variable that is incremented when ref changes so autorun for
@@ -289,7 +296,7 @@ function stateModelFactory() {
289
296
  self.loadingMSA = arg;
290
297
  },
291
298
  /**
292
- * #volatile
299
+ * #action
293
300
  */
294
301
  setShowZoomStar(arg) {
295
302
  self.showZoomStar = arg;
@@ -338,7 +345,7 @@ function stateModelFactory() {
338
345
  return;
339
346
  }
340
347
  // Find the node in the hierarchy
341
- const node = self.hierarchy.find((n) => n.data.id === nodeId);
348
+ const node = self.hierarchy.find(n => n.data.id === nodeId);
342
349
  if (!node) {
343
350
  self.hoveredTreeNode = undefined;
344
351
  return;
@@ -347,6 +354,13 @@ function stateModelFactory() {
347
354
  const descendantNames = node.leaves().map((leaf) => leaf.data.name);
348
355
  self.hoveredTreeNode = { nodeId, descendantNames };
349
356
  },
357
+ /**
358
+ * #action
359
+ * set highlighted columns
360
+ */
361
+ setHighlightedColumns(columns) {
362
+ self.highlightedColumns = columns;
363
+ },
350
364
  /**
351
365
  * #action
352
366
  */
@@ -409,7 +423,7 @@ function stateModelFactory() {
409
423
  /**
410
424
  * #action
411
425
  */
412
- toggleCollapsed2(node) {
426
+ toggleCollapsedLeaf(node) {
413
427
  if (self.collapsedLeaves.includes(node)) {
414
428
  self.collapsedLeaves.remove(node);
415
429
  }
@@ -461,11 +475,21 @@ function stateModelFactory() {
461
475
  },
462
476
  }))
463
477
  .views(self => ({
478
+ /**
479
+ * #getter
480
+ * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
481
+ */
482
+ get hideGapsEffective() {
483
+ return (self.hideGaps &&
484
+ (self.collapsed.length > 0 ||
485
+ self.collapsedLeaves.length > 0 ||
486
+ self.allowedGappyness < 100));
487
+ },
464
488
  /**
465
489
  * #getter
466
490
  */
467
491
  get realAllowedGappyness() {
468
- return self.hideGaps ? self.allowedGappyness : 100;
492
+ return this.hideGapsEffective ? self.allowedGappyness : 100;
469
493
  },
470
494
  /**
471
495
  * #getter
@@ -548,6 +572,9 @@ function stateModelFactory() {
548
572
  if (Stockholm.sniff(text)) {
549
573
  return new StockholmMSA(text, self.currentAlignment);
550
574
  }
575
+ else if (A3mMSA.sniff(text)) {
576
+ return new A3mMSA(text);
577
+ }
551
578
  else if (text.startsWith('>')) {
552
579
  return new FastaMSA(text);
553
580
  }
@@ -595,6 +622,33 @@ function stateModelFactory() {
595
622
  const { mouseRow } = self;
596
623
  return mouseRow === undefined ? undefined : this.rowNames[mouseRow];
597
624
  },
625
+ /**
626
+ * #getter
627
+ * Returns insertion info if mouse is hovering over an insertion indicator
628
+ */
629
+ get hoveredInsertion() {
630
+ const { mouseCol, mouseRow } = self;
631
+ if (mouseCol === undefined || mouseRow === undefined) {
632
+ return undefined;
633
+ }
634
+ const rowName = this.rowNames[mouseRow];
635
+ if (!rowName) {
636
+ return undefined;
637
+ }
638
+ const insertions = this.insertionPositions.get(rowName);
639
+ if (!insertions) {
640
+ return undefined;
641
+ }
642
+ const insertion = insertions.find(ins => ins.pos === mouseCol);
643
+ if (insertion) {
644
+ return {
645
+ rowName,
646
+ col: mouseCol,
647
+ letters: insertion.letters,
648
+ };
649
+ }
650
+ return undefined;
651
+ },
598
652
  /**
599
653
  * #getter
600
654
  */
@@ -615,7 +669,7 @@ function stateModelFactory() {
615
669
  [...self.collapsed, ...self.collapsedLeaves]
616
670
  .map(collapsedId => hier.find(node => node.data.id === collapsedId))
617
671
  .filter(notEmpty)
618
- .map(node => {
672
+ .forEach(node => {
619
673
  collapse(node);
620
674
  });
621
675
  return hier;
@@ -637,28 +691,35 @@ function stateModelFactory() {
637
691
  * #getter
638
692
  */
639
693
  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
- }
694
+ const { hideGapsEffective, realAllowedGappyness } = self;
695
+ if (!hideGapsEffective) {
696
+ return [];
697
+ }
698
+ const strs = this.leaves
699
+ .map(leaf => this.MSA?.getRow(leaf.data.name))
700
+ .filter(notEmpty);
701
+ if (strs.length === 0) {
702
+ return [];
703
+ }
704
+ const numCols = strs[0].length;
705
+ const numRows = strs.length;
706
+ const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
707
+ const blankCounts = new Uint16Array(numCols);
708
+ for (let j = 0; j < numRows; j++) {
709
+ const str = strs[j];
710
+ for (let i = 0; i < numCols; i++) {
711
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
712
+ if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
713
+ blankCounts[i]++;
659
714
  }
660
715
  }
661
716
  }
717
+ const blanks = [];
718
+ for (let i = 0; i < numCols; i++) {
719
+ if (blankCounts[i] >= threshold) {
720
+ blanks.push(i);
721
+ }
722
+ }
662
723
  return blanks;
663
724
  },
664
725
  /**
@@ -667,6 +728,62 @@ function stateModelFactory() {
667
728
  get blanksSet() {
668
729
  return new Set(this.blanks);
669
730
  },
731
+ /**
732
+ * #getter
733
+ * Returns a map of row name to array of insertions with display position and letters
734
+ */
735
+ get insertionPositions() {
736
+ const { hideGapsEffective } = self;
737
+ const { blanks, rows } = this;
738
+ const blanksLen = blanks.length;
739
+ if (blanksLen === 0 || !hideGapsEffective) {
740
+ return new Map();
741
+ }
742
+ const result = new Map();
743
+ for (const [name, seq] of rows) {
744
+ const insertions = [];
745
+ let displayPos = 0;
746
+ let blankIdx = 0;
747
+ let currentInsertPos = -1;
748
+ let letterChars = [];
749
+ const seqLen = seq.length;
750
+ for (let i = 0; i < seqLen; i++) {
751
+ if (blankIdx < blanksLen && blanks[blankIdx] === i) {
752
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
753
+ const code = seq.charCodeAt(i);
754
+ if (!((code - 45) >>> 0 <= 1)) {
755
+ if (currentInsertPos === displayPos) {
756
+ letterChars.push(seq[i]);
757
+ }
758
+ else {
759
+ if (letterChars.length > 0) {
760
+ insertions.push({
761
+ pos: currentInsertPos,
762
+ letters: letterChars.join(''),
763
+ });
764
+ }
765
+ currentInsertPos = displayPos;
766
+ letterChars = [seq[i]];
767
+ }
768
+ }
769
+ blankIdx++;
770
+ }
771
+ else {
772
+ displayPos++;
773
+ }
774
+ }
775
+ if (letterChars.length > 0) {
776
+ insertions.push({
777
+ pos: currentInsertPos,
778
+ letters: letterChars.join(''),
779
+ });
780
+ }
781
+ if (insertions.length > 0) {
782
+ result.set(name, insertions);
783
+ }
784
+ }
785
+ return result;
786
+ },
670
787
  /**
671
788
  * #getter
672
789
  */
@@ -698,10 +815,10 @@ function stateModelFactory() {
698
815
  * #getter
699
816
  */
700
817
  get columns2d() {
701
- const { hideGaps } = self;
818
+ const { hideGapsEffective } = self;
702
819
  return this.rows
703
820
  .map(r => r[1])
704
- .map(str => (hideGaps ? skipBlanks(this.blanks, str) : str));
821
+ .map(str => (hideGapsEffective ? skipBlanks(this.blanks, str) : str));
705
822
  },
706
823
  /**
707
824
  * #getter
@@ -734,6 +851,182 @@ function stateModelFactory() {
734
851
  get colStatsSums() {
735
852
  return this.colStats.map(val => sum(Object.values(val)));
736
853
  },
854
+ /**
855
+ * #getter
856
+ * Pre-computed consensus letter and percent identity color per column.
857
+ * Used by percent_identity_dynamic color scheme.
858
+ */
859
+ get colConsensus() {
860
+ const { colStats, colStatsSums } = this;
861
+ return colStats.map((stats, i) => {
862
+ const total = colStatsSums[i];
863
+ let maxCount = 0;
864
+ let letter = '';
865
+ for (const key in stats) {
866
+ const val = stats[key];
867
+ if (val > maxCount && key !== '-' && key !== '.') {
868
+ maxCount = val;
869
+ letter = key;
870
+ }
871
+ }
872
+ const proportion = maxCount / total;
873
+ return {
874
+ letter,
875
+ color: proportion > 0.4
876
+ ? `hsl(240, 30%, ${100 * Math.max(1 - proportion / 3, 0.3)}%)`
877
+ : undefined,
878
+ };
879
+ });
880
+ },
881
+ /**
882
+ * #getter
883
+ * Pre-computed ClustalX colors per column.
884
+ * Returns a map of letter -> color for each column.
885
+ * ref http://www.jalview.org/help/html/colourSchemes/clustal.html
886
+ */
887
+ get colClustalX() {
888
+ const { colStats, colStatsSums } = this;
889
+ return colStats.map((stats, i) => {
890
+ const total = colStatsSums[i];
891
+ const colors = {};
892
+ const W = stats.W ?? 0;
893
+ const L = stats.L ?? 0;
894
+ const V = stats.V ?? 0;
895
+ const I = stats.I ?? 0;
896
+ const M = stats.M ?? 0;
897
+ const A = stats.A ?? 0;
898
+ const F = stats.F ?? 0;
899
+ const C = stats.C ?? 0;
900
+ const H = stats.H ?? 0;
901
+ const P = stats.P ?? 0;
902
+ const R = stats.R ?? 0;
903
+ const K = stats.K ?? 0;
904
+ const Q = stats.Q ?? 0;
905
+ const E = stats.E ?? 0;
906
+ const D = stats.D ?? 0;
907
+ const T = stats.T ?? 0;
908
+ const S = stats.S ?? 0;
909
+ const G = stats.G ?? 0;
910
+ const Y = stats.Y ?? 0;
911
+ const N = stats.N ?? 0;
912
+ const WLVIMAFCHPY = W + L + V + I + M + A + F + C + H + P + Y;
913
+ const KR = K + R;
914
+ const QE = Q + E;
915
+ const ED = E + D;
916
+ const TS = T + S;
917
+ if (WLVIMAFCHPY / total > 0.6) {
918
+ colors.W = 'rgb(128,179,230)';
919
+ colors.L = 'rgb(128,179,230)';
920
+ colors.V = 'rgb(128,179,230)';
921
+ colors.A = 'rgb(128,179,230)';
922
+ colors.I = 'rgb(128,179,230)';
923
+ colors.M = 'rgb(128,179,230)';
924
+ colors.F = 'rgb(128,179,230)';
925
+ colors.C = 'rgb(128,179,230)';
926
+ }
927
+ if (KR / total > 0.6 ||
928
+ K / total > 0.8 ||
929
+ R / total > 0.8 ||
930
+ Q / total > 0.8) {
931
+ colors.K = '#d88';
932
+ colors.R = '#d88';
933
+ }
934
+ if (KR / total > 0.6 ||
935
+ QE / total > 0.5 ||
936
+ E / total > 0.8 ||
937
+ Q / total > 0.8 ||
938
+ D / total > 0.8) {
939
+ colors.E = 'rgb(192, 72, 192)';
940
+ }
941
+ if (KR / total > 0.6 ||
942
+ ED / total > 0.5 ||
943
+ K / total > 0.8 ||
944
+ R / total > 0.8 ||
945
+ Q / total > 0.8) {
946
+ colors.D = 'rgb(204, 77, 204)';
947
+ }
948
+ if (N / total > 0.5 || Y / total > 0.85) {
949
+ colors.N = '#8f8';
950
+ }
951
+ if (KR / total > 0.6 ||
952
+ QE / total > 0.6 ||
953
+ Q / total > 0.85 ||
954
+ E / total > 0.85 ||
955
+ K / total > 0.85 ||
956
+ R / total > 0.85) {
957
+ colors.Q = '#8f8';
958
+ }
959
+ if (WLVIMAFCHPY / total > 0.6 ||
960
+ TS / total > 0.5 ||
961
+ S / total > 0.85 ||
962
+ T / total > 0.85) {
963
+ colors.S = 'rgb(26,204,26)';
964
+ colors.T = 'rgb(26,204,26)';
965
+ }
966
+ if (C / total > 0.85) {
967
+ colors.C = 'rgb(240, 128, 128)';
968
+ }
969
+ if (G / total > 0) {
970
+ colors.G = 'rgb(240, 144, 72)';
971
+ }
972
+ if (P / total > 0) {
973
+ colors.P = 'rgb(204, 204, 0)';
974
+ }
975
+ if (WLVIMAFCHPY / total > 0.6 ||
976
+ W / total > 0.85 ||
977
+ Y / total > 0.85 ||
978
+ A / total > 0.85 ||
979
+ C / total > 0.85 ||
980
+ P / total > 0.85 ||
981
+ Q / total > 0.85 ||
982
+ F / total > 0.85 ||
983
+ H / total > 0.85 ||
984
+ I / total > 0.85 ||
985
+ L / total > 0.85 ||
986
+ M / total > 0.85 ||
987
+ V / total > 0.85) {
988
+ colors.H = 'rgb(26, 179, 179)';
989
+ colors.Y = 'rgb(26, 179, 179)';
990
+ }
991
+ return colors;
992
+ });
993
+ },
994
+ /**
995
+ * #getter
996
+ * Conservation score per column using Shannon entropy (biojs-msa style).
997
+ * Conservation = (1 - H/Hmax) * (1 - gapFraction)
998
+ * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
999
+ */
1000
+ get conservation() {
1001
+ const { colStats, colStatsSums } = this;
1002
+ const alphabetSize = 20;
1003
+ const maxEntropy = Math.log2(alphabetSize);
1004
+ return colStats.map((stats, i) => {
1005
+ const total = colStatsSums[i];
1006
+ if (!total) {
1007
+ return 0;
1008
+ }
1009
+ const gapCount = (stats['-'] || 0) + (stats['.'] || 0);
1010
+ const nonGapTotal = total - gapCount;
1011
+ if (nonGapTotal === 0) {
1012
+ return 0;
1013
+ }
1014
+ let entropy = 0;
1015
+ for (const letter of Object.keys(stats)) {
1016
+ if (letter === '-' || letter === '.') {
1017
+ continue;
1018
+ }
1019
+ const count = stats[letter];
1020
+ const freq = count / nonGapTotal;
1021
+ if (freq > 0) {
1022
+ entropy -= freq * Math.log2(freq);
1023
+ }
1024
+ }
1025
+ const gapFraction = gapCount / total;
1026
+ const conservation = Math.max(0, 1 - entropy / maxEntropy);
1027
+ return conservation * (1 - gapFraction);
1028
+ });
1029
+ },
737
1030
  /**
738
1031
  * #getter
739
1032
  * generates a new tree that is clustered with x,y positions
@@ -765,6 +1058,13 @@ function stateModelFactory() {
765
1058
  get allBranchesLength0() {
766
1059
  return this.hierarchy.links().every(s => !s.source.data.length);
767
1060
  },
1061
+ /**
1062
+ * #getter
1063
+ * effective showBranchLen accounting for allBranchesLength0
1064
+ */
1065
+ get showBranchLenEffective() {
1066
+ return this.allBranchesLength0 ? false : self.showBranchLen;
1067
+ },
768
1068
  }))
769
1069
  .views(self => ({
770
1070
  /**
@@ -851,6 +1151,17 @@ function stateModelFactory() {
851
1151
  setDrawMsaLetters(arg) {
852
1152
  self.drawMsaLetters = arg;
853
1153
  },
1154
+ /**
1155
+ * #action
1156
+ * Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
1157
+ */
1158
+ calculateNeighborJoiningTreeFromMSA() {
1159
+ if (self.rows.length < 2) {
1160
+ throw new Error('Need at least 2 sequences to build a tree');
1161
+ }
1162
+ const newickTree = calculateNeighborJoiningTree(self.rows);
1163
+ self.setTree(newickTree);
1164
+ },
854
1165
  /**
855
1166
  * #action
856
1167
  */
@@ -972,33 +1283,17 @@ function stateModelFactory() {
972
1283
  get seqConsensus() {
973
1284
  return self.MSA?.seqConsensus;
974
1285
  },
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
1286
  /**
990
1287
  * #getter
991
1288
  */
992
1289
  get adapterTrackModels() {
993
- const { rowHeight, MSA, hideGaps, blanks } = self;
994
- return (MSA?.tracks.map(t => ({
1290
+ const { rowHeight, MSA, hideGapsEffective, blanks } = self;
1291
+ return (MSA?.tracks
1292
+ .filter(t => t.data)
1293
+ .map(t => ({
995
1294
  model: {
996
1295
  ...t,
997
- data: t.data
998
- ? hideGaps
999
- ? skipBlanks(blanks, t.data)
1000
- : t.data
1001
- : undefined,
1296
+ data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
1002
1297
  height: rowHeight,
1003
1298
  },
1004
1299
  ReactComponent: TextTrack,
@@ -1008,7 +1303,15 @@ function stateModelFactory() {
1008
1303
  * #getter
1009
1304
  */
1010
1305
  get tracks() {
1011
- return this.adapterTrackModels;
1306
+ const conservationTrack = {
1307
+ model: {
1308
+ id: 'conservation',
1309
+ name: 'Conservation',
1310
+ height: 40,
1311
+ },
1312
+ ReactComponent: ConservationTrack,
1313
+ };
1314
+ return [...this.adapterTrackModels, conservationTrack];
1012
1315
  },
1013
1316
  /**
1014
1317
  * #getter
@@ -1359,81 +1662,77 @@ function stateModelFactory() {
1359
1662
  }))
1360
1663
  .postProcessSnapshot(result => {
1361
1664
  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);
1665
+ const { data: { tree, msa, treeMetadata },
1666
+ // Main model properties
1667
+ showDomains, hideGaps, allowedGappyness, contrastLettering, subFeatureRows, drawMsaLetters, height, rowHeight, scrollY, scrollX, colWidth, currentAlignment, collapsed, collapsedLeaves, showOnly, turnedOffTracks, featureFilters, relativeTo,
1668
+ // MSA model properties
1669
+ bgColor, colorSchemeName,
1670
+ // Tree model properties
1671
+ drawLabels, labelsAlignRight, treeAreaWidth, treeWidth, treeWidthMatchesArea, showBranchLen, drawTree, drawNodeBubbles,
1672
+ // Always include
1673
+ ...rest } = snap;
1428
1674
  // remove the MSA/tree data from the tree if the filehandle available in
1429
1675
  // which case it can be reloaded on refresh
1430
1676
  return {
1677
+ ...rest,
1431
1678
  data: {
1432
1679
  ...(result.treeFilehandle ? {} : { tree }),
1433
1680
  ...(result.msaFilehandle ? {} : { msa }),
1434
1681
  ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
1435
1682
  },
1436
- ...filteredRest,
1683
+ // Main model - only include non-default values
1684
+ ...(showDomains !== defaultShowDomains ? { showDomains } : {}),
1685
+ ...(hideGaps !== defaultHideGaps ? { hideGaps } : {}),
1686
+ ...(allowedGappyness !== defaultAllowedGappyness
1687
+ ? { allowedGappyness }
1688
+ : {}),
1689
+ ...(contrastLettering !== defaultContrastLettering
1690
+ ? { contrastLettering }
1691
+ : {}),
1692
+ ...(subFeatureRows !== defaultSubFeatureRows ? { subFeatureRows } : {}),
1693
+ ...(drawMsaLetters !== defaultDrawMsaLetters ? { drawMsaLetters } : {}),
1694
+ ...(height !== defaultHeight ? { height } : {}),
1695
+ ...(rowHeight !== defaultRowHeight ? { rowHeight } : {}),
1696
+ ...(scrollY !== defaultScrollY ? { scrollY } : {}),
1697
+ ...(scrollX !== defaultScrollX ? { scrollX } : {}),
1698
+ ...(colWidth !== defaultColWidth ? { colWidth } : {}),
1699
+ ...(currentAlignment !== defaultCurrentAlignment
1700
+ ? { currentAlignment }
1701
+ : {}),
1702
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1703
+ ...(collapsed?.length ? { collapsed } : {}),
1704
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1705
+ ...(collapsedLeaves?.length ? { collapsedLeaves } : {}),
1706
+ ...(showOnly !== undefined ? { showOnly } : {}),
1707
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1708
+ ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
1709
+ ? { turnedOffTracks }
1710
+ : {}),
1711
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1712
+ ...(featureFilters && Object.keys(featureFilters).length > 0
1713
+ ? { featureFilters }
1714
+ : {}),
1715
+ ...(relativeTo !== undefined ? { relativeTo } : {}),
1716
+ // MSA model - only include non-default values
1717
+ ...(bgColor !== defaultBgColor ? { bgColor } : {}),
1718
+ ...(colorSchemeName !== defaultColorSchemeName
1719
+ ? { colorSchemeName }
1720
+ : {}),
1721
+ // Tree model - only include non-default values
1722
+ ...(drawLabels !== defaultDrawLabels ? { drawLabels } : {}),
1723
+ ...(labelsAlignRight !== defaultLabelsAlignRight
1724
+ ? { labelsAlignRight }
1725
+ : {}),
1726
+ ...(treeAreaWidth !== defaultTreeAreaWidth ? { treeAreaWidth } : {}),
1727
+ ...(treeWidth !== defaultTreeWidth ? { treeWidth } : {}),
1728
+ ...(treeWidthMatchesArea !== defaultTreeWidthMatchesArea
1729
+ ? { treeWidthMatchesArea }
1730
+ : {}),
1731
+ ...(showBranchLen !== defaultShowBranchLen ? { showBranchLen } : {}),
1732
+ ...(drawTree !== defaultDrawTree ? { drawTree } : {}),
1733
+ ...(drawNodeBubbles !== defaultDrawNodeBubbles
1734
+ ? { drawNodeBubbles }
1735
+ : {}),
1437
1736
  };
1438
1737
  });
1439
1738
  }