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/src/model.ts CHANGED
@@ -20,7 +20,32 @@ import Stockholm from 'stockholm-js'
20
20
 
21
21
  import { blocksX, blocksY } from './calculateBlocks'
22
22
  import colorSchemes from './colorSchemes'
23
+ import ConservationTrack from './components/ConservationTrack'
23
24
  import TextTrack from './components/TextTrack'
25
+ import {
26
+ defaultAllowedGappyness,
27
+ defaultBgColor,
28
+ defaultColWidth,
29
+ defaultColorSchemeName,
30
+ defaultContrastLettering,
31
+ defaultCurrentAlignment,
32
+ defaultDrawLabels,
33
+ defaultDrawMsaLetters,
34
+ defaultDrawNodeBubbles,
35
+ defaultDrawTree,
36
+ defaultHeight,
37
+ defaultHideGaps,
38
+ defaultLabelsAlignRight,
39
+ defaultRowHeight,
40
+ defaultScrollX,
41
+ defaultScrollY,
42
+ defaultShowBranchLen,
43
+ defaultShowDomains,
44
+ defaultSubFeatureRows,
45
+ defaultTreeAreaWidth,
46
+ defaultTreeWidth,
47
+ defaultTreeWidthMatchesArea,
48
+ } from './constants'
24
49
  import { flatToTree } from './flatToTree'
25
50
  import palettes from './ggplotPalettes'
26
51
  import { measureTextCanvas } from './measureTextCanvas'
@@ -28,8 +53,10 @@ import { DataModelF } from './model/DataModel'
28
53
  import { DialogQueueSessionMixin } from './model/DialogQueue'
29
54
  import { MSAModelF } from './model/msaModel'
30
55
  import { TreeModelF } from './model/treeModel'
56
+ import { calculateNeighborJoiningTree } from './neighborJoining'
31
57
  import { parseAsn1 } from './parseAsn1'
32
58
  import parseNewick from './parseNewick'
59
+ import A3mMSA from './parsers/A3mMSA'
33
60
  import ClustalMSA from './parsers/ClustalMSA'
34
61
  import EmfMSA from './parsers/EmfMSA'
35
62
  import FastaMSA from './parsers/FastaMSA'
@@ -43,7 +70,6 @@ import { seqCoordToRowSpecificGlobalCoord } from './seqCoordToRowSpecificGlobalC
43
70
  import {
44
71
  collapse,
45
72
  generateNodeIds,
46
- isBlank,
47
73
  len,
48
74
  maxLength,
49
75
  setBrLength,
@@ -63,8 +89,6 @@ import type { Theme } from '@mui/material'
63
89
  import type { HierarchyNode } from 'd3-hierarchy'
64
90
  import type { Instance } from 'mobx-state-tree'
65
91
 
66
- const defaultRowHeight = 16
67
- const defaultColWidth = 12
68
92
  const showZoomStarKey = 'msa-showZoomStar'
69
93
 
70
94
  /**
@@ -90,24 +114,24 @@ function stateModelFactory() {
90
114
  /**
91
115
  * #property
92
116
  */
93
- showDomains: false,
117
+ showDomains: defaultShowDomains,
94
118
  /**
95
119
  * #property
96
120
  */
97
- hideGaps: true,
121
+ hideGaps: defaultHideGaps,
98
122
  /**
99
123
  * #property
100
124
  */
101
- allowedGappyness: 100,
125
+ allowedGappyness: defaultAllowedGappyness,
102
126
  /**
103
127
  * #property
104
128
  */
105
- contrastLettering: true,
129
+ contrastLettering: defaultContrastLettering,
106
130
 
107
131
  /**
108
132
  * #property
109
133
  */
110
- subFeatureRows: false,
134
+ subFeatureRows: defaultSubFeatureRows,
111
135
 
112
136
  /**
113
137
  * #property
@@ -118,13 +142,13 @@ function stateModelFactory() {
118
142
  /**
119
143
  * #property
120
144
  */
121
- drawMsaLetters: true,
145
+ drawMsaLetters: defaultDrawMsaLetters,
122
146
 
123
147
  /**
124
148
  * #property
125
149
  * height of the div containing the view, px
126
150
  */
127
- height: types.optional(types.number, 550),
151
+ height: types.optional(types.number, defaultHeight),
128
152
 
129
153
  /**
130
154
  * #property
@@ -136,13 +160,13 @@ function stateModelFactory() {
136
160
  * #property
137
161
  * scroll position, Y-offset, px
138
162
  */
139
- scrollY: 0,
163
+ scrollY: defaultScrollY,
140
164
 
141
165
  /**
142
166
  * #property
143
167
  * scroll position, X-offset, px
144
168
  */
145
- scrollX: 0,
169
+ scrollX: defaultScrollX,
146
170
 
147
171
  /**
148
172
  * #property
@@ -173,7 +197,7 @@ function stateModelFactory() {
173
197
  * #property
174
198
  *
175
199
  */
176
- currentAlignment: 0,
200
+ currentAlignment: defaultCurrentAlignment,
177
201
 
178
202
  /**
179
203
  * #property
@@ -297,6 +321,12 @@ function stateModelFactory() {
297
321
  | { nodeId: string; descendantNames: string[] }
298
322
  | undefined,
299
323
 
324
+ /**
325
+ * #volatile
326
+ * array of column indices to highlight
327
+ */
328
+ highlightedColumns: undefined as number[] | undefined,
329
+
300
330
  /**
301
331
  * #volatile
302
332
  * a dummy variable that is incremented when ref changes so autorun for
@@ -363,7 +393,7 @@ function stateModelFactory() {
363
393
  self.loadingMSA = arg
364
394
  },
365
395
  /**
366
- * #volatile
396
+ * #action
367
397
  */
368
398
  setShowZoomStar(arg: boolean) {
369
399
  self.showZoomStar = arg
@@ -416,8 +446,8 @@ function stateModelFactory() {
416
446
  }
417
447
 
418
448
  // Find the node in the hierarchy
419
- const node = (self as any).hierarchy.find(
420
- (n: any) => n.data.id === nodeId,
449
+ const node = (self as MsaViewModel).hierarchy.find(
450
+ n => n.data.id === nodeId,
421
451
  )
422
452
  if (!node) {
423
453
  self.hoveredTreeNode = undefined
@@ -429,6 +459,13 @@ function stateModelFactory() {
429
459
 
430
460
  self.hoveredTreeNode = { nodeId, descendantNames }
431
461
  },
462
+ /**
463
+ * #action
464
+ * set highlighted columns
465
+ */
466
+ setHighlightedColumns(columns?: number[]) {
467
+ self.highlightedColumns = columns
468
+ },
432
469
  /**
433
470
  * #action
434
471
  */
@@ -497,7 +534,7 @@ function stateModelFactory() {
497
534
  /**
498
535
  * #action
499
536
  */
500
- toggleCollapsed2(node: string) {
537
+ toggleCollapsedLeaf(node: string) {
501
538
  if (self.collapsedLeaves.includes(node)) {
502
539
  self.collapsedLeaves.remove(node)
503
540
  } else {
@@ -555,11 +592,23 @@ function stateModelFactory() {
555
592
  }))
556
593
 
557
594
  .views(self => ({
595
+ /**
596
+ * #getter
597
+ * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
598
+ */
599
+ get hideGapsEffective() {
600
+ return (
601
+ self.hideGaps &&
602
+ (self.collapsed.length > 0 ||
603
+ self.collapsedLeaves.length > 0 ||
604
+ self.allowedGappyness < 100)
605
+ )
606
+ },
558
607
  /**
559
608
  * #getter
560
609
  */
561
610
  get realAllowedGappyness() {
562
- return self.hideGaps ? self.allowedGappyness : 100
611
+ return this.hideGapsEffective ? self.allowedGappyness : 100
563
612
  },
564
613
  /**
565
614
  * #getter
@@ -643,6 +692,8 @@ function stateModelFactory() {
643
692
  if (text) {
644
693
  if (Stockholm.sniff(text)) {
645
694
  return new StockholmMSA(text, self.currentAlignment)
695
+ } else if (A3mMSA.sniff(text)) {
696
+ return new A3mMSA(text)
646
697
  } else if (text.startsWith('>')) {
647
698
  return new FastaMSA(text)
648
699
  } else if (text.startsWith('SEQ')) {
@@ -697,6 +748,33 @@ function stateModelFactory() {
697
748
  const { mouseRow } = self
698
749
  return mouseRow === undefined ? undefined : this.rowNames[mouseRow]
699
750
  },
751
+ /**
752
+ * #getter
753
+ * Returns insertion info if mouse is hovering over an insertion indicator
754
+ */
755
+ get hoveredInsertion() {
756
+ const { mouseCol, mouseRow } = self
757
+ if (mouseCol === undefined || mouseRow === undefined) {
758
+ return undefined
759
+ }
760
+ const rowName = this.rowNames[mouseRow]
761
+ if (!rowName) {
762
+ return undefined
763
+ }
764
+ const insertions = this.insertionPositions.get(rowName)
765
+ if (!insertions) {
766
+ return undefined
767
+ }
768
+ const insertion = insertions.find(ins => ins.pos === mouseCol)
769
+ if (insertion) {
770
+ return {
771
+ rowName,
772
+ col: mouseCol,
773
+ letters: insertion.letters,
774
+ }
775
+ }
776
+ return undefined
777
+ },
700
778
 
701
779
  /**
702
780
  * #getter
@@ -719,7 +797,7 @@ function stateModelFactory() {
719
797
  ;[...self.collapsed, ...self.collapsedLeaves]
720
798
  .map(collapsedId => hier.find(node => node.data.id === collapsedId))
721
799
  .filter(notEmpty)
722
- .map(node => {
800
+ .forEach(node => {
723
801
  collapse(node)
724
802
  })
725
803
 
@@ -744,28 +822,35 @@ function stateModelFactory() {
744
822
  * #getter
745
823
  */
746
824
  get blanks() {
747
- const { hideGaps, realAllowedGappyness } = self
748
- const blanks = []
749
- if (hideGaps) {
750
- const strs = this.leaves
751
- .map(leaf => this.MSA?.getRow(leaf.data.name))
752
- .filter(notEmpty)
753
- if (strs.length) {
754
- const s0len = strs[0]!.length
755
- for (let i = 0; i < s0len; i++) {
756
- let counter = 0
757
- const l = strs.length
758
- for (let j = 0; j < l; j++) {
759
- if (isBlank(strs[j]![i])) {
760
- counter++
761
- }
762
- }
763
- if (counter / l >= realAllowedGappyness / 100) {
764
- blanks.push(i)
765
- }
825
+ const { hideGapsEffective, realAllowedGappyness } = self
826
+ if (!hideGapsEffective) {
827
+ return []
828
+ }
829
+ const strs = this.leaves
830
+ .map(leaf => this.MSA?.getRow(leaf.data.name))
831
+ .filter(notEmpty)
832
+ if (strs.length === 0) {
833
+ return []
834
+ }
835
+ const numCols = strs[0]!.length
836
+ const numRows = strs.length
837
+ const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
838
+ const blankCounts = new Uint16Array(numCols)
839
+ for (let j = 0; j < numRows; j++) {
840
+ const str = strs[j]!
841
+ for (let i = 0; i < numCols; i++) {
842
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
843
+ if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
844
+ blankCounts[i]!++
766
845
  }
767
846
  }
768
847
  }
848
+ const blanks = []
849
+ for (let i = 0; i < numCols; i++) {
850
+ if (blankCounts[i]! >= threshold) {
851
+ blanks.push(i)
852
+ }
853
+ }
769
854
  return blanks
770
855
  },
771
856
  /**
@@ -774,6 +859,60 @@ function stateModelFactory() {
774
859
  get blanksSet() {
775
860
  return new Set(this.blanks)
776
861
  },
862
+ /**
863
+ * #getter
864
+ * Returns a map of row name to array of insertions with display position and letters
865
+ */
866
+ get insertionPositions() {
867
+ const { hideGapsEffective } = self
868
+ const { blanks, rows } = this
869
+ const blanksLen = blanks.length
870
+ if (blanksLen === 0 || !hideGapsEffective) {
871
+ return new Map<string, { pos: number; letters: string }[]>()
872
+ }
873
+ const result = new Map<string, { pos: number; letters: string }[]>()
874
+ for (const [name, seq] of rows) {
875
+ const insertions: { pos: number; letters: string }[] = []
876
+ let displayPos = 0
877
+ let blankIdx = 0
878
+ let currentInsertPos = -1
879
+ let letterChars: string[] = []
880
+ const seqLen = seq.length
881
+ for (let i = 0; i < seqLen; i++) {
882
+ if (blankIdx < blanksLen && blanks[blankIdx] === i) {
883
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
884
+ const code = seq.charCodeAt(i)
885
+ if (!((code - 45) >>> 0 <= 1)) {
886
+ if (currentInsertPos === displayPos) {
887
+ letterChars.push(seq[i]!)
888
+ } else {
889
+ if (letterChars.length > 0) {
890
+ insertions.push({
891
+ pos: currentInsertPos,
892
+ letters: letterChars.join(''),
893
+ })
894
+ }
895
+ currentInsertPos = displayPos
896
+ letterChars = [seq[i]!]
897
+ }
898
+ }
899
+ blankIdx++
900
+ } else {
901
+ displayPos++
902
+ }
903
+ }
904
+ if (letterChars.length > 0) {
905
+ insertions.push({
906
+ pos: currentInsertPos,
907
+ letters: letterChars.join(''),
908
+ })
909
+ }
910
+ if (insertions.length > 0) {
911
+ result.set(name, insertions)
912
+ }
913
+ }
914
+ return result
915
+ },
777
916
  /**
778
917
  * #getter
779
918
  */
@@ -810,10 +949,10 @@ function stateModelFactory() {
810
949
  * #getter
811
950
  */
812
951
  get columns2d() {
813
- const { hideGaps } = self
952
+ const { hideGapsEffective } = self
814
953
  return this.rows
815
954
  .map(r => r[1])
816
- .map(str => (hideGaps ? skipBlanks(this.blanks, str) : str))
955
+ .map(str => (hideGapsEffective ? skipBlanks(this.blanks, str) : str))
817
956
  },
818
957
  /**
819
958
  * #getter
@@ -847,6 +986,216 @@ function stateModelFactory() {
847
986
  get colStatsSums() {
848
987
  return this.colStats.map(val => sum(Object.values(val)))
849
988
  },
989
+
990
+ /**
991
+ * #getter
992
+ * Pre-computed consensus letter and percent identity color per column.
993
+ * Used by percent_identity_dynamic color scheme.
994
+ */
995
+ get colConsensus() {
996
+ const { colStats, colStatsSums } = this
997
+ return colStats.map((stats, i) => {
998
+ const total = colStatsSums[i]!
999
+ let maxCount = 0
1000
+ let letter = ''
1001
+ for (const key in stats) {
1002
+ const val = stats[key]!
1003
+ if (val > maxCount && key !== '-' && key !== '.') {
1004
+ maxCount = val
1005
+ letter = key
1006
+ }
1007
+ }
1008
+ const proportion = maxCount / total
1009
+ return {
1010
+ letter,
1011
+ color:
1012
+ proportion > 0.4
1013
+ ? `hsl(240, 30%, ${100 * Math.max(1 - proportion / 3, 0.3)}%)`
1014
+ : undefined,
1015
+ }
1016
+ })
1017
+ },
1018
+
1019
+ /**
1020
+ * #getter
1021
+ * Pre-computed ClustalX colors per column.
1022
+ * Returns a map of letter -> color for each column.
1023
+ * ref http://www.jalview.org/help/html/colourSchemes/clustal.html
1024
+ */
1025
+ get colClustalX() {
1026
+ const { colStats, colStatsSums } = this
1027
+ return colStats.map((stats, i) => {
1028
+ const total = colStatsSums[i]!
1029
+ const colors: Record<string, string> = {}
1030
+
1031
+ const W = stats.W ?? 0
1032
+ const L = stats.L ?? 0
1033
+ const V = stats.V ?? 0
1034
+ const I = stats.I ?? 0
1035
+ const M = stats.M ?? 0
1036
+ const A = stats.A ?? 0
1037
+ const F = stats.F ?? 0
1038
+ const C = stats.C ?? 0
1039
+ const H = stats.H ?? 0
1040
+ const P = stats.P ?? 0
1041
+ const R = stats.R ?? 0
1042
+ const K = stats.K ?? 0
1043
+ const Q = stats.Q ?? 0
1044
+ const E = stats.E ?? 0
1045
+ const D = stats.D ?? 0
1046
+ const T = stats.T ?? 0
1047
+ const S = stats.S ?? 0
1048
+ const G = stats.G ?? 0
1049
+ const Y = stats.Y ?? 0
1050
+ const N = stats.N ?? 0
1051
+
1052
+ const WLVIMAFCHPY = W + L + V + I + M + A + F + C + H + P + Y
1053
+ const KR = K + R
1054
+ const QE = Q + E
1055
+ const ED = E + D
1056
+ const TS = T + S
1057
+
1058
+ if (WLVIMAFCHPY / total > 0.6) {
1059
+ colors.W = 'rgb(128,179,230)'
1060
+ colors.L = 'rgb(128,179,230)'
1061
+ colors.V = 'rgb(128,179,230)'
1062
+ colors.A = 'rgb(128,179,230)'
1063
+ colors.I = 'rgb(128,179,230)'
1064
+ colors.M = 'rgb(128,179,230)'
1065
+ colors.F = 'rgb(128,179,230)'
1066
+ colors.C = 'rgb(128,179,230)'
1067
+ }
1068
+
1069
+ if (
1070
+ KR / total > 0.6 ||
1071
+ K / total > 0.8 ||
1072
+ R / total > 0.8 ||
1073
+ Q / total > 0.8
1074
+ ) {
1075
+ colors.K = '#d88'
1076
+ colors.R = '#d88'
1077
+ }
1078
+
1079
+ if (
1080
+ KR / total > 0.6 ||
1081
+ QE / total > 0.5 ||
1082
+ E / total > 0.8 ||
1083
+ Q / total > 0.8 ||
1084
+ D / total > 0.8
1085
+ ) {
1086
+ colors.E = 'rgb(192, 72, 192)'
1087
+ }
1088
+
1089
+ if (
1090
+ KR / total > 0.6 ||
1091
+ ED / total > 0.5 ||
1092
+ K / total > 0.8 ||
1093
+ R / total > 0.8 ||
1094
+ Q / total > 0.8
1095
+ ) {
1096
+ colors.D = 'rgb(204, 77, 204)'
1097
+ }
1098
+
1099
+ if (N / total > 0.5 || Y / total > 0.85) {
1100
+ colors.N = '#8f8'
1101
+ }
1102
+
1103
+ if (
1104
+ KR / total > 0.6 ||
1105
+ QE / total > 0.6 ||
1106
+ Q / total > 0.85 ||
1107
+ E / total > 0.85 ||
1108
+ K / total > 0.85 ||
1109
+ R / total > 0.85
1110
+ ) {
1111
+ colors.Q = '#8f8'
1112
+ }
1113
+
1114
+ if (
1115
+ WLVIMAFCHPY / total > 0.6 ||
1116
+ TS / total > 0.5 ||
1117
+ S / total > 0.85 ||
1118
+ T / total > 0.85
1119
+ ) {
1120
+ colors.S = 'rgb(26,204,26)'
1121
+ colors.T = 'rgb(26,204,26)'
1122
+ }
1123
+
1124
+ if (C / total > 0.85) {
1125
+ colors.C = 'rgb(240, 128, 128)'
1126
+ }
1127
+
1128
+ if (G / total > 0) {
1129
+ colors.G = 'rgb(240, 144, 72)'
1130
+ }
1131
+
1132
+ if (P / total > 0) {
1133
+ colors.P = 'rgb(204, 204, 0)'
1134
+ }
1135
+
1136
+ if (
1137
+ WLVIMAFCHPY / total > 0.6 ||
1138
+ W / total > 0.85 ||
1139
+ Y / total > 0.85 ||
1140
+ A / total > 0.85 ||
1141
+ C / total > 0.85 ||
1142
+ P / total > 0.85 ||
1143
+ Q / total > 0.85 ||
1144
+ F / total > 0.85 ||
1145
+ H / total > 0.85 ||
1146
+ I / total > 0.85 ||
1147
+ L / total > 0.85 ||
1148
+ M / total > 0.85 ||
1149
+ V / total > 0.85
1150
+ ) {
1151
+ colors.H = 'rgb(26, 179, 179)'
1152
+ colors.Y = 'rgb(26, 179, 179)'
1153
+ }
1154
+
1155
+ return colors
1156
+ })
1157
+ },
1158
+
1159
+ /**
1160
+ * #getter
1161
+ * Conservation score per column using Shannon entropy (biojs-msa style).
1162
+ * Conservation = (1 - H/Hmax) * (1 - gapFraction)
1163
+ * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
1164
+ */
1165
+ get conservation() {
1166
+ const { colStats, colStatsSums } = this
1167
+ const alphabetSize = 20
1168
+ const maxEntropy = Math.log2(alphabetSize)
1169
+
1170
+ return colStats.map((stats, i) => {
1171
+ const total = colStatsSums[i]
1172
+ if (!total) {
1173
+ return 0
1174
+ }
1175
+
1176
+ const gapCount = (stats['-'] || 0) + (stats['.'] || 0)
1177
+ const nonGapTotal = total - gapCount
1178
+ if (nonGapTotal === 0) {
1179
+ return 0
1180
+ }
1181
+
1182
+ let entropy = 0
1183
+ for (const letter of Object.keys(stats)) {
1184
+ if (letter === '-' || letter === '.') {
1185
+ continue
1186
+ }
1187
+ const count = stats[letter]!
1188
+ const freq = count / nonGapTotal
1189
+ if (freq > 0) {
1190
+ entropy -= freq * Math.log2(freq)
1191
+ }
1192
+ }
1193
+
1194
+ const gapFraction = gapCount / total
1195
+ const conservation = Math.max(0, 1 - entropy / maxEntropy)
1196
+ return conservation * (1 - gapFraction)
1197
+ })
1198
+ },
850
1199
  /**
851
1200
  * #getter
852
1201
  * generates a new tree that is clustered with x,y positions
@@ -881,6 +1230,14 @@ function stateModelFactory() {
881
1230
  get allBranchesLength0() {
882
1231
  return this.hierarchy.links().every(s => !s.source.data.length)
883
1232
  },
1233
+
1234
+ /**
1235
+ * #getter
1236
+ * effective showBranchLen accounting for allBranchesLength0
1237
+ */
1238
+ get showBranchLenEffective() {
1239
+ return this.allBranchesLength0 ? false : self.showBranchLen
1240
+ },
884
1241
  }))
885
1242
  .views(self => ({
886
1243
  /**
@@ -972,6 +1329,18 @@ function stateModelFactory() {
972
1329
  self.drawMsaLetters = arg
973
1330
  },
974
1331
 
1332
+ /**
1333
+ * #action
1334
+ * Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
1335
+ */
1336
+ calculateNeighborJoiningTreeFromMSA() {
1337
+ if (self.rows.length < 2) {
1338
+ throw new Error('Need at least 2 sequences to build a tree')
1339
+ }
1340
+ const newickTree = calculateNeighborJoiningTree(self.rows)
1341
+ self.setTree(newickTree)
1342
+ },
1343
+
975
1344
  /**
976
1345
  * #action
977
1346
  */
@@ -1105,39 +1474,22 @@ function stateModelFactory() {
1105
1474
  return self.MSA?.seqConsensus
1106
1475
  },
1107
1476
 
1108
- /**
1109
- * #getter
1110
- */
1111
- get conservation() {
1112
- if (self.columns2d.length) {
1113
- for (let i = 0; i < self.columns2d[0]!.length; i++) {
1114
- const col = []
1115
- for (const column of self.columns2d) {
1116
- col.push(column[i])
1117
- }
1118
- }
1119
- }
1120
- return ['a']
1121
- },
1122
-
1123
1477
  /**
1124
1478
  * #getter
1125
1479
  */
1126
1480
  get adapterTrackModels(): BasicTrack[] {
1127
- const { rowHeight, MSA, hideGaps, blanks } = self
1481
+ const { rowHeight, MSA, hideGapsEffective, blanks } = self
1128
1482
  return (
1129
- MSA?.tracks.map(t => ({
1130
- model: {
1131
- ...t,
1132
- data: t.data
1133
- ? hideGaps
1134
- ? skipBlanks(blanks, t.data)
1135
- : t.data
1136
- : undefined,
1137
- height: rowHeight,
1138
- } as TextTrackModel,
1139
- ReactComponent: TextTrack,
1140
- })) || []
1483
+ MSA?.tracks
1484
+ .filter(t => t.data)
1485
+ .map(t => ({
1486
+ model: {
1487
+ ...t,
1488
+ data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
1489
+ height: rowHeight,
1490
+ } as TextTrackModel,
1491
+ ReactComponent: TextTrack,
1492
+ })) || []
1141
1493
  )
1142
1494
  },
1143
1495
 
@@ -1145,7 +1497,15 @@ function stateModelFactory() {
1145
1497
  * #getter
1146
1498
  */
1147
1499
  get tracks(): BasicTrack[] {
1148
- return this.adapterTrackModels
1500
+ const conservationTrack: BasicTrack = {
1501
+ model: {
1502
+ id: 'conservation',
1503
+ name: 'Conservation',
1504
+ height: 40,
1505
+ },
1506
+ ReactComponent: ConservationTrack,
1507
+ }
1508
+ return [...this.adapterTrackModels, conservationTrack]
1149
1509
  },
1150
1510
 
1151
1511
  /**
@@ -1374,6 +1734,7 @@ function stateModelFactory() {
1374
1734
  async exportSVG(opts: {
1375
1735
  theme: Theme
1376
1736
  includeMinimap?: boolean
1737
+ includeTracks?: boolean
1377
1738
  exportType: string
1378
1739
  }) {
1379
1740
  const { renderToSvg } = await import('./renderToSvg')
@@ -1556,88 +1917,104 @@ function stateModelFactory() {
1556
1917
  const snap = result as Omit<typeof result, symbol>
1557
1918
  const {
1558
1919
  data: { tree, msa, treeMetadata },
1920
+ // Main model properties
1921
+ showDomains,
1922
+ hideGaps,
1923
+ allowedGappyness,
1924
+ contrastLettering,
1925
+ subFeatureRows,
1926
+ drawMsaLetters,
1927
+ height,
1928
+ rowHeight,
1929
+ scrollY,
1930
+ scrollX,
1931
+ colWidth,
1932
+ currentAlignment,
1933
+ collapsed,
1934
+ collapsedLeaves,
1935
+ showOnly,
1936
+ turnedOffTracks,
1937
+ featureFilters,
1938
+ relativeTo,
1939
+ // MSA model properties
1940
+ bgColor,
1941
+ colorSchemeName,
1942
+ // Tree model properties
1943
+ drawLabels,
1944
+ labelsAlignRight,
1945
+ treeAreaWidth,
1946
+ treeWidth,
1947
+ treeWidthMatchesArea,
1948
+ showBranchLen,
1949
+ drawTree,
1950
+ drawNodeBubbles,
1951
+ // Always include
1559
1952
  ...rest
1560
1953
  } = snap
1561
1954
 
1562
- // Default values to filter out
1563
- const defaults = {
1564
- // Main model defaults
1565
- showDomains: false,
1566
- hideGaps: true,
1567
- allowedGappyness: 100,
1568
- contrastLettering: true,
1569
- subFeatureRows: false,
1570
- drawMsaLetters: true,
1571
- height: 550,
1572
- rowHeight: defaultRowHeight,
1573
- scrollY: 0,
1574
- scrollX: 0,
1575
- colWidth: defaultColWidth,
1576
- currentAlignment: 0,
1577
- // MSA model defaults
1578
- bgColor: true,
1579
- colorSchemeName: 'maeditor',
1580
- // Tree model defaults
1581
- drawLabels: true,
1582
- labelsAlignRight: false,
1583
- treeAreaWidth: 400,
1584
- treeWidth: 300,
1585
- treeWidthMatchesArea: true,
1586
- showBranchLen: true,
1587
- drawTree: true,
1588
- drawNodeBubbles: true,
1589
- }
1590
-
1591
- // Properties that should always be included even if they match defaults
1592
- const alwaysInclude = new Set(['id', 'type', 'relativeTo'])
1593
-
1594
- // Filter out properties that match default values
1595
- function filterDefaults(obj: Record<string, any>): Record<string, any> {
1596
- const filtered: Record<string, any> = {}
1597
- for (const [key, value] of Object.entries(obj)) {
1598
- // Always include essential properties
1599
- if (alwaysInclude.has(key)) {
1600
- filtered[key] = value
1601
- continue
1602
- }
1603
-
1604
- // Skip if value matches default
1605
- if (defaults[key as keyof typeof defaults] === value) {
1606
- continue
1607
- }
1608
-
1609
- // Handle nested objects
1610
- if (value && typeof value === 'object' && !Array.isArray(value)) {
1611
- const filteredNested = filterDefaults(value)
1612
- // Only include nested object if it has non-default properties
1613
- if (Object.keys(filteredNested).length > 0) {
1614
- filtered[key] = filteredNested
1615
- }
1616
- } else if (Array.isArray(value)) {
1617
- // Only include arrays that aren't empty
1618
- if (value.length > 0) {
1619
- filtered[key] = value
1620
- }
1621
- } else {
1622
- // Include non-default primitives
1623
- filtered[key] = value
1624
- }
1625
- }
1626
- return filtered
1627
- }
1628
-
1629
- const filteredRest = filterDefaults(rest)
1630
-
1631
1955
  // remove the MSA/tree data from the tree if the filehandle available in
1632
1956
  // which case it can be reloaded on refresh
1633
1957
  return {
1958
+ ...rest,
1634
1959
  data: {
1635
1960
  ...(result.treeFilehandle ? {} : { tree }),
1636
1961
  ...(result.msaFilehandle ? {} : { msa }),
1637
1962
  ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
1638
1963
  },
1639
- ...filteredRest,
1640
- }
1964
+ // Main model - only include non-default values
1965
+ ...(showDomains !== defaultShowDomains ? { showDomains } : {}),
1966
+ ...(hideGaps !== defaultHideGaps ? { hideGaps } : {}),
1967
+ ...(allowedGappyness !== defaultAllowedGappyness
1968
+ ? { allowedGappyness }
1969
+ : {}),
1970
+ ...(contrastLettering !== defaultContrastLettering
1971
+ ? { contrastLettering }
1972
+ : {}),
1973
+ ...(subFeatureRows !== defaultSubFeatureRows ? { subFeatureRows } : {}),
1974
+ ...(drawMsaLetters !== defaultDrawMsaLetters ? { drawMsaLetters } : {}),
1975
+ ...(height !== defaultHeight ? { height } : {}),
1976
+ ...(rowHeight !== defaultRowHeight ? { rowHeight } : {}),
1977
+ ...(scrollY !== defaultScrollY ? { scrollY } : {}),
1978
+ ...(scrollX !== defaultScrollX ? { scrollX } : {}),
1979
+ ...(colWidth !== defaultColWidth ? { colWidth } : {}),
1980
+ ...(currentAlignment !== defaultCurrentAlignment
1981
+ ? { currentAlignment }
1982
+ : {}),
1983
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1984
+ ...(collapsed?.length ? { collapsed } : {}),
1985
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1986
+ ...(collapsedLeaves?.length ? { collapsedLeaves } : {}),
1987
+ ...(showOnly !== undefined ? { showOnly } : {}),
1988
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1989
+ ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
1990
+ ? { turnedOffTracks }
1991
+ : {}),
1992
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1993
+ ...(featureFilters && Object.keys(featureFilters).length > 0
1994
+ ? { featureFilters }
1995
+ : {}),
1996
+ ...(relativeTo !== undefined ? { relativeTo } : {}),
1997
+ // MSA model - only include non-default values
1998
+ ...(bgColor !== defaultBgColor ? { bgColor } : {}),
1999
+ ...(colorSchemeName !== defaultColorSchemeName
2000
+ ? { colorSchemeName }
2001
+ : {}),
2002
+ // Tree model - only include non-default values
2003
+ ...(drawLabels !== defaultDrawLabels ? { drawLabels } : {}),
2004
+ ...(labelsAlignRight !== defaultLabelsAlignRight
2005
+ ? { labelsAlignRight }
2006
+ : {}),
2007
+ ...(treeAreaWidth !== defaultTreeAreaWidth ? { treeAreaWidth } : {}),
2008
+ ...(treeWidth !== defaultTreeWidth ? { treeWidth } : {}),
2009
+ ...(treeWidthMatchesArea !== defaultTreeWidthMatchesArea
2010
+ ? { treeWidthMatchesArea }
2011
+ : {}),
2012
+ ...(showBranchLen !== defaultShowBranchLen ? { showBranchLen } : {}),
2013
+ ...(drawTree !== defaultDrawTree ? { drawTree } : {}),
2014
+ ...(drawNodeBubbles !== defaultDrawNodeBubbles
2015
+ ? { drawNodeBubbles }
2016
+ : {}),
2017
+ } as typeof snap
1641
2018
  })
1642
2019
  }
1643
2020