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/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
@@ -363,7 +387,7 @@ function stateModelFactory() {
363
387
  self.loadingMSA = arg
364
388
  },
365
389
  /**
366
- * #volatile
390
+ * #action
367
391
  */
368
392
  setShowZoomStar(arg: boolean) {
369
393
  self.showZoomStar = arg
@@ -416,8 +440,8 @@ function stateModelFactory() {
416
440
  }
417
441
 
418
442
  // Find the node in the hierarchy
419
- const node = (self as any).hierarchy.find(
420
- (n: any) => n.data.id === nodeId,
443
+ const node = (self as MsaViewModel).hierarchy.find(
444
+ n => n.data.id === nodeId,
421
445
  )
422
446
  if (!node) {
423
447
  self.hoveredTreeNode = undefined
@@ -497,7 +521,7 @@ function stateModelFactory() {
497
521
  /**
498
522
  * #action
499
523
  */
500
- toggleCollapsed2(node: string) {
524
+ toggleCollapsedLeaf(node: string) {
501
525
  if (self.collapsedLeaves.includes(node)) {
502
526
  self.collapsedLeaves.remove(node)
503
527
  } else {
@@ -555,11 +579,23 @@ function stateModelFactory() {
555
579
  }))
556
580
 
557
581
  .views(self => ({
582
+ /**
583
+ * #getter
584
+ * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
585
+ */
586
+ get hideGapsEffective() {
587
+ return (
588
+ self.hideGaps &&
589
+ (self.collapsed.length > 0 ||
590
+ self.collapsedLeaves.length > 0 ||
591
+ self.allowedGappyness < 100)
592
+ )
593
+ },
558
594
  /**
559
595
  * #getter
560
596
  */
561
597
  get realAllowedGappyness() {
562
- return self.hideGaps ? self.allowedGappyness : 100
598
+ return this.hideGapsEffective ? self.allowedGappyness : 100
563
599
  },
564
600
  /**
565
601
  * #getter
@@ -643,6 +679,8 @@ function stateModelFactory() {
643
679
  if (text) {
644
680
  if (Stockholm.sniff(text)) {
645
681
  return new StockholmMSA(text, self.currentAlignment)
682
+ } else if (A3mMSA.sniff(text)) {
683
+ return new A3mMSA(text)
646
684
  } else if (text.startsWith('>')) {
647
685
  return new FastaMSA(text)
648
686
  } else if (text.startsWith('SEQ')) {
@@ -697,6 +735,33 @@ function stateModelFactory() {
697
735
  const { mouseRow } = self
698
736
  return mouseRow === undefined ? undefined : this.rowNames[mouseRow]
699
737
  },
738
+ /**
739
+ * #getter
740
+ * Returns insertion info if mouse is hovering over an insertion indicator
741
+ */
742
+ get hoveredInsertion() {
743
+ const { mouseCol, mouseRow } = self
744
+ if (mouseCol === undefined || mouseRow === undefined) {
745
+ return undefined
746
+ }
747
+ const rowName = this.rowNames[mouseRow]
748
+ if (!rowName) {
749
+ return undefined
750
+ }
751
+ const insertions = this.insertionPositions.get(rowName)
752
+ if (!insertions) {
753
+ return undefined
754
+ }
755
+ const insertion = insertions.find(ins => ins.pos === mouseCol)
756
+ if (insertion) {
757
+ return {
758
+ rowName,
759
+ col: mouseCol,
760
+ letters: insertion.letters,
761
+ }
762
+ }
763
+ return undefined
764
+ },
700
765
 
701
766
  /**
702
767
  * #getter
@@ -719,7 +784,7 @@ function stateModelFactory() {
719
784
  ;[...self.collapsed, ...self.collapsedLeaves]
720
785
  .map(collapsedId => hier.find(node => node.data.id === collapsedId))
721
786
  .filter(notEmpty)
722
- .map(node => {
787
+ .forEach(node => {
723
788
  collapse(node)
724
789
  })
725
790
 
@@ -744,28 +809,35 @@ function stateModelFactory() {
744
809
  * #getter
745
810
  */
746
811
  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
- }
812
+ const { hideGapsEffective, realAllowedGappyness } = self
813
+ if (!hideGapsEffective) {
814
+ return []
815
+ }
816
+ const strs = this.leaves
817
+ .map(leaf => this.MSA?.getRow(leaf.data.name))
818
+ .filter(notEmpty)
819
+ if (strs.length === 0) {
820
+ return []
821
+ }
822
+ const numCols = strs[0]!.length
823
+ const numRows = strs.length
824
+ const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
825
+ const blankCounts = new Uint16Array(numCols)
826
+ for (let j = 0; j < numRows; j++) {
827
+ const str = strs[j]!
828
+ for (let i = 0; i < numCols; i++) {
829
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
830
+ if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
831
+ blankCounts[i]!++
766
832
  }
767
833
  }
768
834
  }
835
+ const blanks = []
836
+ for (let i = 0; i < numCols; i++) {
837
+ if (blankCounts[i]! >= threshold) {
838
+ blanks.push(i)
839
+ }
840
+ }
769
841
  return blanks
770
842
  },
771
843
  /**
@@ -774,6 +846,60 @@ function stateModelFactory() {
774
846
  get blanksSet() {
775
847
  return new Set(this.blanks)
776
848
  },
849
+ /**
850
+ * #getter
851
+ * Returns a map of row name to array of insertions with display position and letters
852
+ */
853
+ get insertionPositions() {
854
+ const { hideGapsEffective } = self
855
+ const { blanks, rows } = this
856
+ const blanksLen = blanks.length
857
+ if (blanksLen === 0 || !hideGapsEffective) {
858
+ return new Map<string, { pos: number; letters: string }[]>()
859
+ }
860
+ const result = new Map<string, { pos: number; letters: string }[]>()
861
+ for (const [name, seq] of rows) {
862
+ const insertions: { pos: number; letters: string }[] = []
863
+ let displayPos = 0
864
+ let blankIdx = 0
865
+ let currentInsertPos = -1
866
+ let letterChars: string[] = []
867
+ const seqLen = seq.length
868
+ for (let i = 0; i < seqLen; i++) {
869
+ if (blankIdx < blanksLen && blanks[blankIdx] === i) {
870
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
871
+ const code = seq.charCodeAt(i)
872
+ if (!((code - 45) >>> 0 <= 1)) {
873
+ if (currentInsertPos === displayPos) {
874
+ letterChars.push(seq[i]!)
875
+ } else {
876
+ if (letterChars.length > 0) {
877
+ insertions.push({
878
+ pos: currentInsertPos,
879
+ letters: letterChars.join(''),
880
+ })
881
+ }
882
+ currentInsertPos = displayPos
883
+ letterChars = [seq[i]!]
884
+ }
885
+ }
886
+ blankIdx++
887
+ } else {
888
+ displayPos++
889
+ }
890
+ }
891
+ if (letterChars.length > 0) {
892
+ insertions.push({
893
+ pos: currentInsertPos,
894
+ letters: letterChars.join(''),
895
+ })
896
+ }
897
+ if (insertions.length > 0) {
898
+ result.set(name, insertions)
899
+ }
900
+ }
901
+ return result
902
+ },
777
903
  /**
778
904
  * #getter
779
905
  */
@@ -810,10 +936,10 @@ function stateModelFactory() {
810
936
  * #getter
811
937
  */
812
938
  get columns2d() {
813
- const { hideGaps } = self
939
+ const { hideGapsEffective } = self
814
940
  return this.rows
815
941
  .map(r => r[1])
816
- .map(str => (hideGaps ? skipBlanks(this.blanks, str) : str))
942
+ .map(str => (hideGapsEffective ? skipBlanks(this.blanks, str) : str))
817
943
  },
818
944
  /**
819
945
  * #getter
@@ -847,6 +973,216 @@ function stateModelFactory() {
847
973
  get colStatsSums() {
848
974
  return this.colStats.map(val => sum(Object.values(val)))
849
975
  },
976
+
977
+ /**
978
+ * #getter
979
+ * Pre-computed consensus letter and percent identity color per column.
980
+ * Used by percent_identity_dynamic color scheme.
981
+ */
982
+ get colConsensus() {
983
+ const { colStats, colStatsSums } = this
984
+ return colStats.map((stats, i) => {
985
+ const total = colStatsSums[i]!
986
+ let maxCount = 0
987
+ let letter = ''
988
+ for (const key in stats) {
989
+ const val = stats[key]!
990
+ if (val > maxCount && key !== '-' && key !== '.') {
991
+ maxCount = val
992
+ letter = key
993
+ }
994
+ }
995
+ const proportion = maxCount / total
996
+ return {
997
+ letter,
998
+ color:
999
+ proportion > 0.4
1000
+ ? `hsl(240, 30%, ${100 * Math.max(1 - proportion / 3, 0.3)}%)`
1001
+ : undefined,
1002
+ }
1003
+ })
1004
+ },
1005
+
1006
+ /**
1007
+ * #getter
1008
+ * Pre-computed ClustalX colors per column.
1009
+ * Returns a map of letter -> color for each column.
1010
+ * ref http://www.jalview.org/help/html/colourSchemes/clustal.html
1011
+ */
1012
+ get colClustalX() {
1013
+ const { colStats, colStatsSums } = this
1014
+ return colStats.map((stats, i) => {
1015
+ const total = colStatsSums[i]!
1016
+ const colors: Record<string, string> = {}
1017
+
1018
+ const W = stats.W ?? 0
1019
+ const L = stats.L ?? 0
1020
+ const V = stats.V ?? 0
1021
+ const I = stats.I ?? 0
1022
+ const M = stats.M ?? 0
1023
+ const A = stats.A ?? 0
1024
+ const F = stats.F ?? 0
1025
+ const C = stats.C ?? 0
1026
+ const H = stats.H ?? 0
1027
+ const P = stats.P ?? 0
1028
+ const R = stats.R ?? 0
1029
+ const K = stats.K ?? 0
1030
+ const Q = stats.Q ?? 0
1031
+ const E = stats.E ?? 0
1032
+ const D = stats.D ?? 0
1033
+ const T = stats.T ?? 0
1034
+ const S = stats.S ?? 0
1035
+ const G = stats.G ?? 0
1036
+ const Y = stats.Y ?? 0
1037
+ const N = stats.N ?? 0
1038
+
1039
+ const WLVIMAFCHPY = W + L + V + I + M + A + F + C + H + P + Y
1040
+ const KR = K + R
1041
+ const QE = Q + E
1042
+ const ED = E + D
1043
+ const TS = T + S
1044
+
1045
+ if (WLVIMAFCHPY / total > 0.6) {
1046
+ colors.W = 'rgb(128,179,230)'
1047
+ colors.L = 'rgb(128,179,230)'
1048
+ colors.V = 'rgb(128,179,230)'
1049
+ colors.A = 'rgb(128,179,230)'
1050
+ colors.I = 'rgb(128,179,230)'
1051
+ colors.M = 'rgb(128,179,230)'
1052
+ colors.F = 'rgb(128,179,230)'
1053
+ colors.C = 'rgb(128,179,230)'
1054
+ }
1055
+
1056
+ if (
1057
+ KR / total > 0.6 ||
1058
+ K / total > 0.8 ||
1059
+ R / total > 0.8 ||
1060
+ Q / total > 0.8
1061
+ ) {
1062
+ colors.K = '#d88'
1063
+ colors.R = '#d88'
1064
+ }
1065
+
1066
+ if (
1067
+ KR / total > 0.6 ||
1068
+ QE / total > 0.5 ||
1069
+ E / total > 0.8 ||
1070
+ Q / total > 0.8 ||
1071
+ D / total > 0.8
1072
+ ) {
1073
+ colors.E = 'rgb(192, 72, 192)'
1074
+ }
1075
+
1076
+ if (
1077
+ KR / total > 0.6 ||
1078
+ ED / total > 0.5 ||
1079
+ K / total > 0.8 ||
1080
+ R / total > 0.8 ||
1081
+ Q / total > 0.8
1082
+ ) {
1083
+ colors.D = 'rgb(204, 77, 204)'
1084
+ }
1085
+
1086
+ if (N / total > 0.5 || Y / total > 0.85) {
1087
+ colors.N = '#8f8'
1088
+ }
1089
+
1090
+ if (
1091
+ KR / total > 0.6 ||
1092
+ QE / total > 0.6 ||
1093
+ Q / total > 0.85 ||
1094
+ E / total > 0.85 ||
1095
+ K / total > 0.85 ||
1096
+ R / total > 0.85
1097
+ ) {
1098
+ colors.Q = '#8f8'
1099
+ }
1100
+
1101
+ if (
1102
+ WLVIMAFCHPY / total > 0.6 ||
1103
+ TS / total > 0.5 ||
1104
+ S / total > 0.85 ||
1105
+ T / total > 0.85
1106
+ ) {
1107
+ colors.S = 'rgb(26,204,26)'
1108
+ colors.T = 'rgb(26,204,26)'
1109
+ }
1110
+
1111
+ if (C / total > 0.85) {
1112
+ colors.C = 'rgb(240, 128, 128)'
1113
+ }
1114
+
1115
+ if (G / total > 0) {
1116
+ colors.G = 'rgb(240, 144, 72)'
1117
+ }
1118
+
1119
+ if (P / total > 0) {
1120
+ colors.P = 'rgb(204, 204, 0)'
1121
+ }
1122
+
1123
+ if (
1124
+ WLVIMAFCHPY / total > 0.6 ||
1125
+ W / total > 0.85 ||
1126
+ Y / total > 0.85 ||
1127
+ A / total > 0.85 ||
1128
+ C / total > 0.85 ||
1129
+ P / total > 0.85 ||
1130
+ Q / total > 0.85 ||
1131
+ F / total > 0.85 ||
1132
+ H / total > 0.85 ||
1133
+ I / total > 0.85 ||
1134
+ L / total > 0.85 ||
1135
+ M / total > 0.85 ||
1136
+ V / total > 0.85
1137
+ ) {
1138
+ colors.H = 'rgb(26, 179, 179)'
1139
+ colors.Y = 'rgb(26, 179, 179)'
1140
+ }
1141
+
1142
+ return colors
1143
+ })
1144
+ },
1145
+
1146
+ /**
1147
+ * #getter
1148
+ * Conservation score per column using Shannon entropy (biojs-msa style).
1149
+ * Conservation = (1 - H/Hmax) * (1 - gapFraction)
1150
+ * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
1151
+ */
1152
+ get conservation() {
1153
+ const { colStats, colStatsSums } = this
1154
+ const alphabetSize = 20
1155
+ const maxEntropy = Math.log2(alphabetSize)
1156
+
1157
+ return colStats.map((stats, i) => {
1158
+ const total = colStatsSums[i]
1159
+ if (!total) {
1160
+ return 0
1161
+ }
1162
+
1163
+ const gapCount = (stats['-'] || 0) + (stats['.'] || 0)
1164
+ const nonGapTotal = total - gapCount
1165
+ if (nonGapTotal === 0) {
1166
+ return 0
1167
+ }
1168
+
1169
+ let entropy = 0
1170
+ for (const letter of Object.keys(stats)) {
1171
+ if (letter === '-' || letter === '.') {
1172
+ continue
1173
+ }
1174
+ const count = stats[letter]!
1175
+ const freq = count / nonGapTotal
1176
+ if (freq > 0) {
1177
+ entropy -= freq * Math.log2(freq)
1178
+ }
1179
+ }
1180
+
1181
+ const gapFraction = gapCount / total
1182
+ const conservation = Math.max(0, 1 - entropy / maxEntropy)
1183
+ return conservation * (1 - gapFraction)
1184
+ })
1185
+ },
850
1186
  /**
851
1187
  * #getter
852
1188
  * generates a new tree that is clustered with x,y positions
@@ -972,6 +1308,18 @@ function stateModelFactory() {
972
1308
  self.drawMsaLetters = arg
973
1309
  },
974
1310
 
1311
+ /**
1312
+ * #action
1313
+ * Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
1314
+ */
1315
+ calculateNeighborJoiningTreeFromMSA() {
1316
+ if (self.rows.length < 2) {
1317
+ throw new Error('Need at least 2 sequences to build a tree')
1318
+ }
1319
+ const newickTree = calculateNeighborJoiningTree(self.rows)
1320
+ self.setTree(newickTree)
1321
+ },
1322
+
975
1323
  /**
976
1324
  * #action
977
1325
  */
@@ -1105,39 +1453,22 @@ function stateModelFactory() {
1105
1453
  return self.MSA?.seqConsensus
1106
1454
  },
1107
1455
 
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
1456
  /**
1124
1457
  * #getter
1125
1458
  */
1126
1459
  get adapterTrackModels(): BasicTrack[] {
1127
- const { rowHeight, MSA, hideGaps, blanks } = self
1460
+ const { rowHeight, MSA, hideGapsEffective, blanks } = self
1128
1461
  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
- })) || []
1462
+ MSA?.tracks
1463
+ .filter(t => t.data)
1464
+ .map(t => ({
1465
+ model: {
1466
+ ...t,
1467
+ data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
1468
+ height: rowHeight,
1469
+ } as TextTrackModel,
1470
+ ReactComponent: TextTrack,
1471
+ })) || []
1141
1472
  )
1142
1473
  },
1143
1474
 
@@ -1145,7 +1476,15 @@ function stateModelFactory() {
1145
1476
  * #getter
1146
1477
  */
1147
1478
  get tracks(): BasicTrack[] {
1148
- return this.adapterTrackModels
1479
+ const conservationTrack: BasicTrack = {
1480
+ model: {
1481
+ id: 'conservation',
1482
+ name: 'Conservation',
1483
+ height: 40,
1484
+ },
1485
+ ReactComponent: ConservationTrack,
1486
+ }
1487
+ return [...this.adapterTrackModels, conservationTrack]
1149
1488
  },
1150
1489
 
1151
1490
  /**
@@ -1374,6 +1713,7 @@ function stateModelFactory() {
1374
1713
  async exportSVG(opts: {
1375
1714
  theme: Theme
1376
1715
  includeMinimap?: boolean
1716
+ includeTracks?: boolean
1377
1717
  exportType: string
1378
1718
  }) {
1379
1719
  const { renderToSvg } = await import('./renderToSvg')
@@ -1556,88 +1896,104 @@ function stateModelFactory() {
1556
1896
  const snap = result as Omit<typeof result, symbol>
1557
1897
  const {
1558
1898
  data: { tree, msa, treeMetadata },
1899
+ // Main model properties
1900
+ showDomains,
1901
+ hideGaps,
1902
+ allowedGappyness,
1903
+ contrastLettering,
1904
+ subFeatureRows,
1905
+ drawMsaLetters,
1906
+ height,
1907
+ rowHeight,
1908
+ scrollY,
1909
+ scrollX,
1910
+ colWidth,
1911
+ currentAlignment,
1912
+ collapsed,
1913
+ collapsedLeaves,
1914
+ showOnly,
1915
+ turnedOffTracks,
1916
+ featureFilters,
1917
+ relativeTo,
1918
+ // MSA model properties
1919
+ bgColor,
1920
+ colorSchemeName,
1921
+ // Tree model properties
1922
+ drawLabels,
1923
+ labelsAlignRight,
1924
+ treeAreaWidth,
1925
+ treeWidth,
1926
+ treeWidthMatchesArea,
1927
+ showBranchLen,
1928
+ drawTree,
1929
+ drawNodeBubbles,
1930
+ // Always include
1559
1931
  ...rest
1560
1932
  } = snap
1561
1933
 
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
1934
  // remove the MSA/tree data from the tree if the filehandle available in
1632
1935
  // which case it can be reloaded on refresh
1633
1936
  return {
1937
+ ...rest,
1634
1938
  data: {
1635
1939
  ...(result.treeFilehandle ? {} : { tree }),
1636
1940
  ...(result.msaFilehandle ? {} : { msa }),
1637
1941
  ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
1638
1942
  },
1639
- ...filteredRest,
1640
- }
1943
+ // Main model - only include non-default values
1944
+ ...(showDomains !== defaultShowDomains ? { showDomains } : {}),
1945
+ ...(hideGaps !== defaultHideGaps ? { hideGaps } : {}),
1946
+ ...(allowedGappyness !== defaultAllowedGappyness
1947
+ ? { allowedGappyness }
1948
+ : {}),
1949
+ ...(contrastLettering !== defaultContrastLettering
1950
+ ? { contrastLettering }
1951
+ : {}),
1952
+ ...(subFeatureRows !== defaultSubFeatureRows ? { subFeatureRows } : {}),
1953
+ ...(drawMsaLetters !== defaultDrawMsaLetters ? { drawMsaLetters } : {}),
1954
+ ...(height !== defaultHeight ? { height } : {}),
1955
+ ...(rowHeight !== defaultRowHeight ? { rowHeight } : {}),
1956
+ ...(scrollY !== defaultScrollY ? { scrollY } : {}),
1957
+ ...(scrollX !== defaultScrollX ? { scrollX } : {}),
1958
+ ...(colWidth !== defaultColWidth ? { colWidth } : {}),
1959
+ ...(currentAlignment !== defaultCurrentAlignment
1960
+ ? { currentAlignment }
1961
+ : {}),
1962
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1963
+ ...(collapsed?.length ? { collapsed } : {}),
1964
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1965
+ ...(collapsedLeaves?.length ? { collapsedLeaves } : {}),
1966
+ ...(showOnly !== undefined ? { showOnly } : {}),
1967
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1968
+ ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
1969
+ ? { turnedOffTracks }
1970
+ : {}),
1971
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1972
+ ...(featureFilters && Object.keys(featureFilters).length > 0
1973
+ ? { featureFilters }
1974
+ : {}),
1975
+ ...(relativeTo !== undefined ? { relativeTo } : {}),
1976
+ // MSA model - only include non-default values
1977
+ ...(bgColor !== defaultBgColor ? { bgColor } : {}),
1978
+ ...(colorSchemeName !== defaultColorSchemeName
1979
+ ? { colorSchemeName }
1980
+ : {}),
1981
+ // Tree model - only include non-default values
1982
+ ...(drawLabels !== defaultDrawLabels ? { drawLabels } : {}),
1983
+ ...(labelsAlignRight !== defaultLabelsAlignRight
1984
+ ? { labelsAlignRight }
1985
+ : {}),
1986
+ ...(treeAreaWidth !== defaultTreeAreaWidth ? { treeAreaWidth } : {}),
1987
+ ...(treeWidth !== defaultTreeWidth ? { treeWidth } : {}),
1988
+ ...(treeWidthMatchesArea !== defaultTreeWidthMatchesArea
1989
+ ? { treeWidthMatchesArea }
1990
+ : {}),
1991
+ ...(showBranchLen !== defaultShowBranchLen ? { showBranchLen } : {}),
1992
+ ...(drawTree !== defaultDrawTree ? { drawTree } : {}),
1993
+ ...(drawNodeBubbles !== defaultDrawNodeBubbles
1994
+ ? { drawNodeBubbles }
1995
+ : {}),
1996
+ } as typeof snap
1641
1997
  })
1642
1998
  }
1643
1999