react-msaview 5.4.1 → 5.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 (180) hide show
  1. package/LICENSE +21 -0
  2. package/bundle/index.js +487 -149
  3. package/bundle/index.js.map +7 -1
  4. package/dist/colorSchemes.js +11 -7
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/ConservationTrack.js +20 -26
  7. package/dist/components/ConservationTrack.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +13 -1
  9. package/dist/components/MSAViewer.js +13 -7
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/SequenceTextArea.js +1 -1
  12. package/dist/components/SequenceTextArea.js.map +1 -1
  13. package/dist/components/TextTrack.js +20 -27
  14. package/dist/components/TextTrack.js.map +1 -1
  15. package/dist/components/Track.js +9 -6
  16. package/dist/components/Track.js.map +1 -1
  17. package/dist/components/dialogs/FeatureDialog.js +27 -23
  18. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  19. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  20. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.js +1 -1
  22. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  23. package/dist/components/header/FileMenu.js +1 -16
  24. package/dist/components/header/FileMenu.js.map +1 -1
  25. package/dist/components/header/Header.js +6 -5
  26. package/dist/components/header/Header.js.map +1 -1
  27. package/dist/components/header/HeaderInfoArea.js +1 -1
  28. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  29. package/dist/components/header/HeaderStatusArea.js +1 -1
  30. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  31. package/dist/components/header/MSASettingsMenu.js +1 -1
  32. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  33. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  34. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -1
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +14 -12
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/ZoomMenu.js +1 -10
  40. package/dist/components/header/ZoomMenu.js.map +1 -1
  41. package/dist/components/header/settingsMenuItems.js +9 -1
  42. package/dist/components/header/settingsMenuItems.js.map +1 -1
  43. package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
  44. package/dist/components/msa/ColumnStats.js +42 -0
  45. package/dist/components/msa/ColumnStats.js.map +1 -0
  46. package/dist/components/msa/DomainLegend.d.ts +6 -0
  47. package/dist/components/msa/DomainLegend.js +41 -0
  48. package/dist/components/msa/DomainLegend.js.map +1 -0
  49. package/dist/components/msa/MSACanvasBlock.js +41 -29
  50. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  51. package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
  52. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  53. package/dist/components/msa/MSAPanel.js +3 -1
  54. package/dist/components/msa/MSAPanel.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +60 -14
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  57. package/dist/components/msa/renderMSABlock.js +3 -3
  58. package/dist/components/msa/renderMSABlock.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +33 -6
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/overlayColors.d.ts +3 -0
  62. package/dist/components/overlayColors.js +11 -0
  63. package/dist/components/overlayColors.js.map +1 -0
  64. package/dist/components/renderCtx.d.ts +1 -1
  65. package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
  66. package/dist/components/tracks/renderTracksSvg.js +20 -11
  67. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  68. package/dist/components/tree/TreeCanvas.js +31 -38
  69. package/dist/components/tree/TreeCanvas.js.map +1 -1
  70. package/dist/components/tree/TreeCanvasBlock.js +12 -47
  71. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  72. package/dist/components/tree/clickMap.d.ts +24 -0
  73. package/dist/components/tree/clickMap.js +33 -0
  74. package/dist/components/tree/clickMap.js.map +1 -0
  75. package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
  76. package/dist/components/tree/renderTreeCanvas.js +8 -7
  77. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  78. package/dist/constants.d.ts +4 -0
  79. package/dist/constants.js +17 -0
  80. package/dist/constants.js.map +1 -1
  81. package/dist/createPaletteMap.d.ts +5 -2
  82. package/dist/createPaletteMap.js +15 -5
  83. package/dist/createPaletteMap.js.map +1 -1
  84. package/dist/fetchUtils.d.ts +2 -1
  85. package/dist/fetchUtils.js +3 -2
  86. package/dist/fetchUtils.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/launchInterProScan.d.ts +2 -0
  89. package/dist/launchInterProScan.js.map +1 -1
  90. package/dist/model/msaModel.d.ts +25 -2
  91. package/dist/model/msaModel.js +15 -3
  92. package/dist/model/msaModel.js.map +1 -1
  93. package/dist/model/treeModel.d.ts +46 -5
  94. package/dist/model/treeModel.js +20 -7
  95. package/dist/model/treeModel.js.map +1 -1
  96. package/dist/model.d.ts +641 -133
  97. package/dist/model.js +191 -77
  98. package/dist/model.js.map +1 -1
  99. package/dist/propertyConservation.d.ts +7 -0
  100. package/dist/propertyConservation.js +66 -0
  101. package/dist/propertyConservation.js.map +1 -0
  102. package/dist/renderToSvg.js +104 -74
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/types.d.ts +4 -1
  105. package/dist/{webpack.js → umd.js} +1 -1
  106. package/dist/umd.js.map +1 -0
  107. package/dist/useCanvasAutorun.d.ts +2 -0
  108. package/dist/useCanvasAutorun.js +23 -0
  109. package/dist/useCanvasAutorun.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +7 -8
  113. package/src/colorSchemes.docs.test.ts +55 -0
  114. package/src/colorSchemes.ts +12 -7
  115. package/src/columnStats.test.ts +94 -0
  116. package/src/components/ConservationTrack.tsx +17 -15
  117. package/src/components/MSAViewer.tsx +42 -18
  118. package/src/components/SequenceTextArea.tsx +1 -1
  119. package/src/components/TextTrack.tsx +7 -11
  120. package/src/components/Track.tsx +9 -6
  121. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  122. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  123. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  124. package/src/components/header/FileMenu.tsx +1 -15
  125. package/src/components/header/Header.tsx +11 -8
  126. package/src/components/header/HeaderInfoArea.tsx +1 -1
  127. package/src/components/header/HeaderStatusArea.tsx +1 -1
  128. package/src/components/header/MSASettingsMenu.tsx +1 -0
  129. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  130. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  131. package/src/components/header/ZoomControls.tsx +24 -17
  132. package/src/components/header/ZoomMenu.tsx +1 -10
  133. package/src/components/header/settingsMenuItems.ts +9 -1
  134. package/src/components/msa/ColumnStats.tsx +54 -0
  135. package/src/components/msa/DomainLegend.tsx +78 -0
  136. package/src/components/msa/MSACanvasBlock.tsx +46 -25
  137. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
  138. package/src/components/msa/MSAPanel.tsx +2 -0
  139. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +75 -17
  140. package/src/components/msa/renderMSABlock.ts +3 -3
  141. package/src/components/msa/renderMSAMouseover.ts +34 -6
  142. package/src/components/overlayColors.ts +12 -0
  143. package/src/components/renderCtx.ts +1 -0
  144. package/src/components/tracks/renderTracksSvg.ts +25 -11
  145. package/src/components/tree/TreeCanvas.tsx +47 -54
  146. package/src/components/tree/TreeCanvasBlock.tsx +10 -68
  147. package/src/components/tree/clickMap.ts +55 -0
  148. package/src/components/tree/renderTreeCanvas.ts +10 -34
  149. package/src/constants.ts +19 -0
  150. package/src/createPaletteMap.ts +18 -5
  151. package/src/fetchUtils.ts +6 -3
  152. package/src/index.ts +1 -1
  153. package/src/launchInterProScan.ts +3 -1
  154. package/src/model/msaModel.ts +21 -3
  155. package/src/model/treeModel.ts +21 -7
  156. package/src/model.ts +221 -119
  157. package/src/propertyConservation.test.ts +47 -0
  158. package/src/propertyConservation.ts +73 -0
  159. package/src/renderDomainLegendSvg.test.tsx +134 -0
  160. package/src/renderToSvg.tsx +210 -147
  161. package/src/segmentOverlay.test.ts +70 -0
  162. package/src/stripDefaultSnapshot.test.ts +54 -0
  163. package/src/types.ts +7 -1
  164. package/src/useCanvasAutorun.ts +28 -0
  165. package/src/version.ts +1 -1
  166. package/bundle/index.js.LICENSE.txt +0 -100
  167. package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
  168. package/dist/components/dialogs/SettingsDialog.js +0 -100
  169. package/dist/components/dialogs/SettingsDialog.js.map +0 -1
  170. package/dist/components/header/HeaderMenu.js +0 -27
  171. package/dist/components/header/HeaderMenu.js.map +0 -1
  172. package/dist/components/util.d.ts +0 -1
  173. package/dist/components/util.js +0 -7
  174. package/dist/components/util.js.map +0 -1
  175. package/dist/webpack.js.map +0 -1
  176. package/src/components/dialogs/SettingsDialog.tsx +0 -234
  177. package/src/components/header/HeaderMenu.tsx +0 -38
  178. package/src/components/util.ts +0 -9
  179. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  180. /package/src/{webpack.ts → umd.ts} +0 -0
@@ -19,25 +19,25 @@ export function TreeModelF() {
19
19
  /**
20
20
  * #property
21
21
  */
22
- drawLabels: defaultDrawLabels,
22
+ drawLabels: types.stripDefault(types.boolean, defaultDrawLabels),
23
23
  /**
24
24
  * #property
25
25
  * right-align the labels
26
26
  */
27
- labelsAlignRight: defaultLabelsAlignRight,
27
+ labelsAlignRight: types.stripDefault(types.boolean, defaultLabelsAlignRight),
28
28
 
29
29
  /**
30
30
  * #property
31
31
  * width of the area the tree is drawn in, px
32
32
  */
33
- treeAreaWidth: types.optional(types.number, defaultTreeAreaWidth),
33
+ treeAreaWidth: types.stripDefault(types.number, defaultTreeAreaWidth),
34
34
 
35
35
  /**
36
36
  * #property
37
37
  * width of the tree within the treeArea, px. automatically synced to
38
38
  * fit within treeAreaWidth
39
39
  */
40
- treeWidth: types.optional(types.number, defaultTreeWidth),
40
+ treeWidth: types.stripDefault(types.number, defaultTreeWidth),
41
41
 
42
42
  /**
43
43
  * #property
@@ -45,19 +45,27 @@ export function TreeModelF() {
45
45
  * lengths. if false, the layout is a "cladogram" that does not take into
46
46
  * account evolutionary distances
47
47
  */
48
- showBranchLen: defaultShowBranchLen,
48
+ showBranchLen: types.stripDefault(types.boolean, defaultShowBranchLen),
49
49
 
50
50
  /**
51
51
  * #property
52
52
  * draw tree, boolean
53
53
  */
54
- drawTree: defaultDrawTree,
54
+ drawTree: types.stripDefault(types.boolean, defaultDrawTree),
55
55
 
56
56
  /**
57
57
  * #property
58
58
  * draw clickable node bubbles on the tree
59
59
  */
60
- drawNodeBubbles: defaultDrawNodeBubbles,
60
+ drawNodeBubbles: types.stripDefault(types.boolean, defaultDrawNodeBubbles),
61
+
62
+ /**
63
+ * #property
64
+ * auto-size treeAreaWidth to fit the row labels (plus the tree, if drawn)
65
+ * instead of using a fixed width. useful when there is no tree, so the
66
+ * label gutter isn't padded out to the default 400px
67
+ */
68
+ autoTreeAreaWidth: types.stripDefault(types.boolean, false),
61
69
  })
62
70
  .actions(self => ({
63
71
  /**
@@ -87,6 +95,12 @@ export function TreeModelF() {
87
95
  setDrawTree(arg: boolean) {
88
96
  self.drawTree = arg
89
97
  },
98
+ /**
99
+ * #action
100
+ */
101
+ setAutoTreeAreaWidth(arg: boolean) {
102
+ self.autoTreeAreaWidth = arg
103
+ },
90
104
 
91
105
  /**
92
106
  * #action
package/src/model.ts CHANGED
@@ -20,29 +20,23 @@ import ConservationTrack from './components/ConservationTrack.tsx'
20
20
  import TextTrack from './components/TextTrack.tsx'
21
21
  import {
22
22
  defaultAllowedGappyness,
23
- defaultBgColor,
24
23
  defaultColWidth,
25
24
  defaultColorSchemeName,
26
25
  defaultCurrentAlignment,
27
- defaultDrawLabels,
28
26
  defaultDrawMsaLetters,
29
- defaultDrawNodeBubbles,
30
- defaultDrawTree,
31
27
  defaultHeight,
32
28
  defaultHideGaps,
33
- defaultLabelsAlignRight,
34
29
  defaultRowHeight,
35
30
  defaultScrollX,
36
31
  defaultScrollY,
37
32
  defaultScrollZoom,
38
- defaultShowBranchLen,
39
33
  defaultShowDomains,
40
34
  defaultSubFeatureRows,
41
- defaultTreeAreaWidth,
42
- defaultTreeWidth,
43
35
  maxCellSize,
44
36
  minColWidth,
45
37
  minRowHeight,
38
+ segmentFeatureTypes,
39
+ segmentShades,
46
40
  } from './constants.ts'
47
41
  import { createPaletteMap } from './createPaletteMap.ts'
48
42
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
@@ -70,6 +64,7 @@ import { MSAModelF } from './model/msaModel.ts'
70
64
  import { TreeModelF } from './model/treeModel.ts'
71
65
  import { calculateNeighborJoiningTree } from './neighborJoining.ts'
72
66
  import { parseAsn1 } from './parseAsn1.ts'
67
+ import { calculatePropertyConservation } from './propertyConservation.ts'
73
68
  import {
74
69
  globalColToVisibleCol,
75
70
  visibleColToGlobalCol,
@@ -95,6 +90,14 @@ function parseTreeText(text: string) {
95
90
 
96
91
  /**
97
92
  * #stateModel MsaView
93
+ *
94
+ * The main MSAView state model. Holds the loaded alignment, tree, and optional
95
+ * InterProScan domain annotations, plus all display state (color scheme, zoom,
96
+ * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
97
+ * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
98
+ * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
99
+ * directly with `setData`. Most state is persisted into the shareable URL.
100
+ *
98
101
  * #example
99
102
  * ```js
100
103
  * import { MSAModelF } from 'react-msaview'
@@ -121,19 +124,22 @@ function stateModelFactory() {
121
124
  /**
122
125
  * #property
123
126
  */
124
- showDomains: defaultShowDomains,
127
+ showDomains: types.stripDefault(types.boolean, defaultShowDomains),
125
128
  /**
126
129
  * #property
127
130
  */
128
- hideGaps: defaultHideGaps,
131
+ hideGaps: types.stripDefault(types.boolean, defaultHideGaps),
129
132
  /**
130
133
  * #property
131
134
  */
132
- allowedGappyness: defaultAllowedGappyness,
135
+ allowedGappyness: types.stripDefault(
136
+ types.number,
137
+ defaultAllowedGappyness,
138
+ ),
133
139
  /**
134
140
  * #property
135
141
  */
136
- subFeatureRows: defaultSubFeatureRows,
142
+ subFeatureRows: types.stripDefault(types.boolean, defaultSubFeatureRows),
137
143
 
138
144
  /**
139
145
  * #property
@@ -144,43 +150,43 @@ function stateModelFactory() {
144
150
  /**
145
151
  * #property
146
152
  */
147
- drawMsaLetters: defaultDrawMsaLetters,
153
+ drawMsaLetters: types.stripDefault(types.boolean, defaultDrawMsaLetters),
148
154
 
149
155
  /**
150
156
  * #property
151
157
  * zoom in/out on plain mouse-wheel without holding ctrl
152
158
  */
153
- scrollZoom: defaultScrollZoom,
159
+ scrollZoom: types.stripDefault(types.boolean, defaultScrollZoom),
154
160
 
155
161
  /**
156
162
  * #property
157
163
  * height of the div containing the view, px
158
164
  */
159
- height: types.optional(types.number, defaultHeight),
165
+ height: types.stripDefault(types.number, defaultHeight),
160
166
 
161
167
  /**
162
168
  * #property
163
169
  * height of each row, px
164
170
  */
165
- rowHeight: defaultRowHeight,
171
+ rowHeight: types.stripDefault(types.number, defaultRowHeight),
166
172
 
167
173
  /**
168
174
  * #property
169
175
  * scroll position, Y-offset, px
170
176
  */
171
- scrollY: defaultScrollY,
177
+ scrollY: types.stripDefault(types.number, defaultScrollY),
172
178
 
173
179
  /**
174
180
  * #property
175
181
  * scroll position, X-offset, px
176
182
  */
177
- scrollX: defaultScrollX,
183
+ scrollX: types.stripDefault(types.number, defaultScrollX),
178
184
 
179
185
  /**
180
186
  * #property
181
187
  * width of columns, px
182
188
  */
183
- colWidth: defaultColWidth,
189
+ colWidth: types.stripDefault(types.number, defaultColWidth),
184
190
 
185
191
  /**
186
192
  * #property
@@ -210,14 +216,17 @@ function stateModelFactory() {
210
216
  /**
211
217
  * #property
212
218
  */
213
- currentAlignment: defaultCurrentAlignment,
219
+ currentAlignment: types.stripDefault(
220
+ types.number,
221
+ defaultCurrentAlignment,
222
+ ),
214
223
 
215
224
  /**
216
225
  * #property
217
226
  * array of tree parent nodes that are 'collapsed' (all children are
218
227
  * hidden)
219
228
  */
220
- collapsed: types.array(types.string),
229
+ collapsed: types.stripDefault(types.array(types.string), []),
221
230
 
222
231
  /**
223
232
  * #property
@@ -228,7 +237,7 @@ function stateModelFactory() {
228
237
  * #property
229
238
  * turned off tracks
230
239
  */
231
- turnedOffTracks: types.map(types.boolean),
240
+ turnedOffTracks: types.stripDefault(types.map(types.boolean), {}),
232
241
 
233
242
  /**
234
243
  * #property
@@ -244,7 +253,7 @@ function stateModelFactory() {
244
253
  /**
245
254
  * #property
246
255
  */
247
- featureFilters: types.map(types.boolean),
256
+ featureFilters: types.stripDefault(types.map(types.boolean), {}),
248
257
  /**
249
258
  * #property
250
259
  */
@@ -269,8 +278,7 @@ function stateModelFactory() {
269
278
  * #volatile
270
279
  */
271
280
  status: undefined as
272
- | { msg: string; url?: string; onCancel?: () => void }
273
- | undefined,
281
+ { msg: string; url?: string; onCancel?: () => void } | undefined,
274
282
  /**
275
283
  * #volatile
276
284
  * high resolution scale factor, helps make canvas look better on hi-dpi
@@ -333,8 +341,7 @@ function stateModelFactory() {
333
341
  * the currently hovered tree node ID and its descendant leaf names
334
342
  */
335
343
  hoveredTreeNode: undefined as
336
- | { nodeId: string; descendantNames: string[] }
337
- | undefined,
344
+ { nodeId: string; descendantNames: string[] } | undefined,
338
345
 
339
346
  /**
340
347
  * #volatile
@@ -367,18 +374,20 @@ function stateModelFactory() {
367
374
  * #volatile
368
375
  */
369
376
  interProAnnotations: undefined as
370
- | undefined
371
- | Record<string, InterProScanResults>,
377
+ undefined | Record<string, InterProScanResults>,
372
378
  }))
373
379
  .actions(self => ({
374
380
  /**
375
381
  * #action
382
+ * draw the alignment with positions numbered relative to the given row's
383
+ * sequence (its node id), instead of in raw MSA-column coordinates
376
384
  */
377
385
  drawRelativeTo(id: string | undefined) {
378
386
  self.relativeTo = id
379
387
  },
380
388
  /**
381
389
  * #action
390
+ * hide columns that are entirely (or mostly, see allowedGappyness) gaps
382
391
  */
383
392
  setHideGaps(arg: boolean) {
384
393
  self.hideGaps = arg
@@ -457,6 +466,7 @@ function stateModelFactory() {
457
466
  },
458
467
  /**
459
468
  * #action
469
+ * toggle the InterProScan protein-domain overlay on the alignment
460
470
  */
461
471
  setShowDomains(arg: boolean) {
462
472
  self.showDomains = arg
@@ -510,6 +520,7 @@ function stateModelFactory() {
510
520
 
511
521
  /**
512
522
  * #action
523
+ * collapse or un-collapse the subtree rooted at the given tree node id
513
524
  */
514
525
  toggleCollapsed(node: string) {
515
526
  if (self.collapsed.includes(node)) {
@@ -521,6 +532,8 @@ function stateModelFactory() {
521
532
 
522
533
  /**
523
534
  * #action
535
+ * show only the subtree rooted at the given node id (pass undefined to
536
+ * show the whole tree again)
524
537
  */
525
538
  setShowOnly(node?: string) {
526
539
  self.showOnly = node
@@ -528,6 +541,8 @@ function stateModelFactory() {
528
541
 
529
542
  /**
530
543
  * #action
544
+ * set the alignment/tree/metadata/domain data directly from strings,
545
+ * bypassing the filehandle loaders
531
546
  */
532
547
  setData(data: {
533
548
  msa?: string
@@ -1025,6 +1040,17 @@ function stateModelFactory() {
1025
1040
  return conservation * (1 - gapFraction)
1026
1041
  })
1027
1042
  },
1043
+ /**
1044
+ * #getter
1045
+ * Per-column conservation of physicochemical property class (amino acids
1046
+ * only). Surfaces conservative-substitution sites that identity-based
1047
+ * conservation misses. Empty for nucleotide alignments.
1048
+ */
1049
+ get propertyConservation() {
1050
+ return this.sequenceType === 'amino'
1051
+ ? calculatePropertyConservation(this.colStats, this.colStatsSums)
1052
+ : []
1053
+ },
1028
1054
  /**
1029
1055
  * #getter
1030
1056
  * generates a new tree that is clustered with x,y positions
@@ -1232,6 +1258,7 @@ function stateModelFactory() {
1232
1258
 
1233
1259
  /**
1234
1260
  * #action
1261
+ * restore the default column width and row height
1235
1262
  */
1236
1263
  resetZoom() {
1237
1264
  self.setColWidth(defaultColWidth)
@@ -1446,10 +1473,24 @@ function stateModelFactory() {
1446
1473
  id: 'conservation',
1447
1474
  name: 'Conservation',
1448
1475
  height: self.conservationTrackHeight,
1476
+ barColor: 'gray',
1449
1477
  },
1450
1478
  ReactComponent: ConservationTrack,
1451
1479
  }
1452
- return [...this.adapterTrackModels, conservationTrack]
1480
+ const propertyConservationTrack: BasicTrack = {
1481
+ model: {
1482
+ id: 'property-conservation',
1483
+ name: 'Property conservation',
1484
+ height: self.conservationTrackHeight,
1485
+ barColor: '#6a51a3',
1486
+ },
1487
+ ReactComponent: ConservationTrack,
1488
+ }
1489
+ return [
1490
+ ...this.adapterTrackModels,
1491
+ conservationTrack,
1492
+ ...(self.sequenceType === 'amino' ? [propertyConservationTrack] : []),
1493
+ ]
1453
1494
  },
1454
1495
 
1455
1496
  /**
@@ -1592,13 +1633,15 @@ function stateModelFactory() {
1592
1633
  val.matches.flatMap(({ signature, locations }) => {
1593
1634
  const { entry } = signature
1594
1635
  return entry
1595
- ? locations.map(({ start, end }) => ({
1636
+ ? locations.map(({ start, end, strand }) => ({
1596
1637
  id,
1597
1638
  name: entry.name,
1598
1639
  accession: entry.accession,
1599
1640
  description: entry.description,
1641
+ featureType: entry.featureType,
1600
1642
  start,
1601
1643
  end,
1644
+ strand,
1602
1645
  }))
1603
1646
  : []
1604
1647
  }),
@@ -1630,8 +1673,38 @@ function stateModelFactory() {
1630
1673
  get verticalScrollbarWidth() {
1631
1674
  return self.showVerticalScrollbar ? 20 : 0
1632
1675
  },
1676
+ /**
1677
+ * #getter
1678
+ * ordinal segment types (exons etc.), ordered by sequence position so
1679
+ * exon-1..exon-14 read left-to-right; colored by alternating shade and
1680
+ * labeled by number rather than each getting a distinct hue + legend row
1681
+ */
1682
+ get segmentDomainTypes() {
1683
+ return [...self.tidyInterProAnnotationTypes.values()]
1684
+ .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1685
+ .toSorted((a, b) => a.start - b.start)
1686
+ },
1687
+ /**
1688
+ * #getter
1689
+ * categorical feature types (InterPro domains and the like) that each get
1690
+ * their own color and a legend entry
1691
+ */
1692
+ get categoricalDomainTypes() {
1693
+ return [...self.tidyInterProAnnotationTypes.values()].filter(
1694
+ d => !segmentFeatureTypes.has(d.featureType ?? ''),
1695
+ )
1696
+ },
1633
1697
  get fillPalette() {
1634
- return createPaletteMap([...self.tidyInterProAnnotationTypes.keys()])
1698
+ const segments = Object.fromEntries(
1699
+ this.segmentDomainTypes.map((d, i) => [
1700
+ d.accession,
1701
+ segmentShades[i % segmentShades.length]!,
1702
+ ]),
1703
+ )
1704
+ const categorical = createPaletteMap(
1705
+ this.categoricalDomainTypes.map(d => d.accession),
1706
+ )
1707
+ return { ...segments, ...categorical }
1635
1708
  },
1636
1709
  get strokePalette() {
1637
1710
  return transform(this.fillPalette, ([key, val]) => [
@@ -1640,6 +1713,92 @@ function stateModelFactory() {
1640
1713
  ])
1641
1714
  },
1642
1715
 
1716
+ /**
1717
+ * #getter
1718
+ * accession -> number drawn on each segment band: the trailing number of
1719
+ * the feature name ("exon-3" -> "3"), else its 1-based position
1720
+ */
1721
+ get segmentLabels() {
1722
+ return new Map(
1723
+ this.segmentDomainTypes.map((d, i) => {
1724
+ const m = /(\d+)\s*$/.exec(d.name)
1725
+ return [d.accession, m ? m[1]! : `${i + 1}`]
1726
+ }),
1727
+ )
1728
+ },
1729
+
1730
+ /**
1731
+ * #getter
1732
+ * the domain types currently drawn on the alignment (filtered-on), shared
1733
+ * by the on-screen legend and the SVG export legend. Ordinal segments
1734
+ * (exons) are excluded — they read as a numbered gene model, not a color
1735
+ * key — so this is the categorical types ordered by sequence position
1736
+ */
1737
+ get visibleDomainTypes() {
1738
+ return this.categoricalDomainTypes
1739
+ .filter(d => self.featureFilters.get(d.accession))
1740
+ .toSorted((a, b) => a.start - b.start)
1741
+ },
1742
+
1743
+ /**
1744
+ * #getter
1745
+ * domain annotations under the mouse, hit-tested against the exact visible
1746
+ * column span each box is drawn at (so it matches the overlay across gaps)
1747
+ */
1748
+ get mouseOverDomains() {
1749
+ const { mouseCol } = self
1750
+ const name = self.mouseOverRowName
1751
+ if (name !== undefined && mouseCol !== undefined) {
1752
+ const entry = self.tidyFilteredGatheredInterProAnnotations[name] ?? []
1753
+ return entry.filter(d => {
1754
+ const m1 = self.seqPosToVisibleCol(name, d.start - 1)
1755
+ const m2 = self.seqPosToVisibleCol(name, d.end)
1756
+ return (
1757
+ m1 !== undefined &&
1758
+ m2 !== undefined &&
1759
+ mouseCol >= m1 &&
1760
+ mouseCol < m2
1761
+ )
1762
+ })
1763
+ }
1764
+ return []
1765
+ },
1766
+
1767
+ /**
1768
+ * #getter
1769
+ * per-column summary statistics for the hovered column: consensus residue
1770
+ * and its identity fraction, conservation score, gap fraction, and the
1771
+ * sorted non-gap residue distribution. undefined when nothing is hovered.
1772
+ */
1773
+ get mouseOverColumnStats() {
1774
+ const { mouseCol } = self
1775
+ if (mouseCol === undefined) {
1776
+ return undefined
1777
+ }
1778
+ const stats = self.colStats[mouseCol]
1779
+ const total = self.colStatsSums[mouseCol]
1780
+ if (!stats || !total) {
1781
+ return undefined
1782
+ }
1783
+ const gaps = (stats['-'] ?? 0) + (stats['.'] ?? 0)
1784
+ const distribution = Object.entries(stats)
1785
+ .filter(([letter]) => letter !== '-' && letter !== '.')
1786
+ .sort((a, b) => b[1] - a[1])
1787
+ const consensus = distribution[0]
1788
+ return {
1789
+ col: mouseCol,
1790
+ total,
1791
+ gaps,
1792
+ gapFraction: gaps / total,
1793
+ conservation: self.conservation[mouseCol] ?? 0,
1794
+ propertyConservation: self.propertyConservation[mouseCol],
1795
+ consensusLetter: consensus?.[0] ?? '',
1796
+ consensusCount: consensus?.[1] ?? 0,
1797
+ consensusFraction: consensus ? consensus[1] / total : 0,
1798
+ distribution,
1799
+ }
1800
+ },
1801
+
1643
1802
  /**
1644
1803
  * #method
1645
1804
  */
@@ -1943,6 +2102,23 @@ function stateModelFactory() {
1943
2102
  }),
1944
2103
  )
1945
2104
 
2105
+ // autorun: when autoTreeAreaWidth is set and no tree is drawn, shrink the
2106
+ // tree area to fit the row labels rather than padding it to the fixed
2107
+ // default. Gated on noTree/!drawTree so it never fights the treeWidth sync
2108
+ // below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
2109
+ addDisposer(
2110
+ self,
2111
+ autorun(() => {
2112
+ if (
2113
+ self.autoTreeAreaWidth &&
2114
+ (self.noTree || !self.drawTree) &&
2115
+ self.labelsWidth
2116
+ ) {
2117
+ self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12)
2118
+ }
2119
+ }),
2120
+ )
2121
+
1946
2122
  // autorun synchronizes treeWidth with treeAreaWidth
1947
2123
  addDisposer(
1948
2124
  self,
@@ -1957,93 +2133,19 @@ function stateModelFactory() {
1957
2133
  )
1958
2134
  },
1959
2135
  }))
1960
- .postProcessSnapshot(result => {
1961
- const snap = result as Omit<typeof result, symbol>
1962
- const {
1963
- data: { tree, msa, treeMetadata },
1964
- // Main model properties
1965
- showDomains,
1966
- hideGaps,
1967
- allowedGappyness,
1968
- subFeatureRows,
1969
- drawMsaLetters,
1970
- height,
1971
- rowHeight,
1972
- scrollY,
1973
- scrollX,
1974
- colWidth,
1975
- currentAlignment,
1976
- collapsed,
1977
- showOnly,
1978
- turnedOffTracks,
1979
- featureFilters,
1980
- relativeTo,
1981
- // MSA model properties
1982
- bgColor,
1983
- colorSchemeName,
1984
- // Tree model properties
1985
- drawLabels,
1986
- labelsAlignRight,
1987
- treeAreaWidth,
1988
- treeWidth,
1989
- showBranchLen,
1990
- drawTree,
1991
- drawNodeBubbles,
1992
- // Always include
1993
- ...rest
1994
- } = snap
1995
-
1996
- const defaults: Record<string, unknown> = {
1997
- showDomains: defaultShowDomains,
1998
- hideGaps: defaultHideGaps,
1999
- allowedGappyness: defaultAllowedGappyness,
2000
- subFeatureRows: defaultSubFeatureRows,
2001
- drawMsaLetters: defaultDrawMsaLetters,
2002
- height: defaultHeight,
2003
- rowHeight: defaultRowHeight,
2004
- scrollY: defaultScrollY,
2005
- scrollX: defaultScrollX,
2006
- colWidth: defaultColWidth,
2007
- currentAlignment: defaultCurrentAlignment,
2008
- bgColor: defaultBgColor,
2009
- colorSchemeName: defaultColorSchemeName,
2010
- drawLabels: defaultDrawLabels,
2011
- labelsAlignRight: defaultLabelsAlignRight,
2012
- treeAreaWidth: defaultTreeAreaWidth,
2013
- treeWidth: defaultTreeWidth,
2014
- showBranchLen: defaultShowBranchLen,
2015
- drawTree: defaultDrawTree,
2016
- drawNodeBubbles: defaultDrawNodeBubbles,
2017
- }
2018
-
2019
- const nonDefaults = Object.fromEntries(
2020
- Object.entries(defaults)
2021
- .filter(([key, def]) => snap[key as keyof typeof snap] !== def)
2022
- .map(([key]) => [key, snap[key as keyof typeof snap]]),
2023
- )
2024
-
2025
- return {
2026
- ...rest,
2027
- data: {
2028
- ...(result.treeFilehandle ? {} : { tree }),
2029
- ...(result.msaFilehandle ? {} : { msa }),
2030
- ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
2031
- },
2032
- ...nonDefaults,
2033
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2034
- ...(collapsed?.length ? { collapsed } : {}),
2035
- ...(showOnly !== undefined ? { showOnly } : {}),
2036
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2037
- ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
2038
- ? { turnedOffTracks }
2039
- : {}),
2040
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2041
- ...(featureFilters && Object.keys(featureFilters).length > 0
2042
- ? { featureFilters }
2043
- : {}),
2044
- ...(relativeTo !== undefined ? { relativeTo } : {}),
2045
- } as typeof snap
2046
- })
2136
+ .postProcessSnapshot(({ data, ...rest }) => ({
2137
+ // per-property defaults are stripped by types.stripDefault; the only thing
2138
+ // it can't express is this cross-field rule: drop inline tree/msa/metadata
2139
+ // when a sibling filehandle can refetch them, keeping sessions/URLs small
2140
+ ...rest,
2141
+ data: {
2142
+ ...(rest.treeFilehandle ? {} : { tree: data.tree }),
2143
+ ...(rest.msaFilehandle ? {} : { msa: data.msa }),
2144
+ ...(rest.treeMetadataFilehandle
2145
+ ? {}
2146
+ : { treeMetadata: data.treeMetadata }),
2147
+ },
2148
+ }))
2047
2149
  }
2048
2150
 
2049
2151
  export default stateModelFactory
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { calculatePropertyConservation } from './propertyConservation.ts'
4
+
5
+ function counts(letters: string) {
6
+ const stats: Record<string, number> = {}
7
+ for (const c of letters) {
8
+ stats[c] = (stats[c] ?? 0) + 1
9
+ }
10
+ return stats
11
+ }
12
+
13
+ function propertyConservation(columns: string[]) {
14
+ const colStats = columns.map(counts)
15
+ const colStatsSums = colStats.map(s =>
16
+ Object.values(s).reduce((a, b) => a + b, 0),
17
+ )
18
+ return calculatePropertyConservation(colStats, colStatsSums)
19
+ }
20
+
21
+ describe('property conservation', () => {
22
+ test('an invariant column scores 1', () => {
23
+ expect(propertyConservation(['LLLL'])[0]).toBe(1)
24
+ })
25
+
26
+ test('a column varying only within one property class stays fully conserved', () => {
27
+ // L/I/V/M are all hydrophobic: identity varies but the property does not
28
+ expect(propertyConservation(['LIVM'])[0]).toBe(1)
29
+ })
30
+
31
+ test('mixing opposite-charge classes lowers the score', () => {
32
+ // K/R (positive) vs D/E (negative): two equally-sized classes
33
+ const score = propertyConservation(['KRDE'])[0]!
34
+ expect(score).toBeGreaterThan(0)
35
+ expect(score).toBeLessThan(1)
36
+ })
37
+
38
+ test('gaps reduce the score proportionally', () => {
39
+ const full = propertyConservation(['LLLL'])[0]!
40
+ const halfGapped = propertyConservation(['LL--'])[0]!
41
+ expect(halfGapped).toBeCloseTo(full * 0.5)
42
+ })
43
+
44
+ test('an all-gap column scores 0', () => {
45
+ expect(propertyConservation(['----'])[0]).toBe(0)
46
+ })
47
+ })