react-msaview 5.5.0 → 5.6.1

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 (139) hide show
  1. package/bundle/index.js +173 -162
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/ConservationTrack.js +20 -26
  4. package/dist/components/ConservationTrack.js.map +1 -1
  5. package/dist/components/MSAViewer.d.ts +7 -1
  6. package/dist/components/MSAViewer.js +4 -1
  7. package/dist/components/MSAViewer.js.map +1 -1
  8. package/dist/components/SequenceTextArea.js +1 -1
  9. package/dist/components/SequenceTextArea.js.map +1 -1
  10. package/dist/components/TextTrack.js +20 -27
  11. package/dist/components/TextTrack.js.map +1 -1
  12. package/dist/components/Track.js +1 -1
  13. package/dist/components/Track.js.map +1 -1
  14. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  15. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  16. package/dist/components/header/ColorSchemeMenu.js +1 -1
  17. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  18. package/dist/components/header/FileMenu.js +1 -16
  19. package/dist/components/header/FileMenu.js.map +1 -1
  20. package/dist/components/header/Header.js +6 -5
  21. package/dist/components/header/Header.js.map +1 -1
  22. package/dist/components/header/HeaderInfoArea.js +1 -1
  23. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  24. package/dist/components/header/HeaderStatusArea.js +1 -1
  25. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  26. package/dist/components/header/MSASettingsMenu.js +1 -1
  27. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  28. package/dist/components/header/TreeSettingsMenu.js +1 -1
  29. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  30. package/dist/components/header/ZoomControls.js +14 -12
  31. package/dist/components/header/ZoomControls.js.map +1 -1
  32. package/dist/components/header/ZoomMenu.js +1 -10
  33. package/dist/components/header/ZoomMenu.js.map +1 -1
  34. package/dist/components/header/settingsMenuItems.js +9 -1
  35. package/dist/components/header/settingsMenuItems.js.map +1 -1
  36. package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
  37. package/dist/components/msa/ColumnStats.js +42 -0
  38. package/dist/components/msa/ColumnStats.js.map +1 -0
  39. package/dist/components/msa/DomainLegend.js +1 -1
  40. package/dist/components/msa/DomainLegend.js.map +1 -1
  41. package/dist/components/msa/MSACanvasBlock.js +39 -39
  42. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  43. package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
  44. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  45. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +3 -1
  46. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  47. package/dist/components/msa/renderMSAMouseover.js +1 -2
  48. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  49. package/dist/components/overlayColors.d.ts +3 -0
  50. package/dist/components/overlayColors.js +11 -0
  51. package/dist/components/overlayColors.js.map +1 -0
  52. package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
  53. package/dist/components/tracks/renderTracksSvg.js +17 -8
  54. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  55. package/dist/components/tree/TreeCanvas.js +31 -38
  56. package/dist/components/tree/TreeCanvas.js.map +1 -1
  57. package/dist/components/tree/TreeCanvasBlock.js +12 -47
  58. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  59. package/dist/components/tree/clickMap.d.ts +24 -0
  60. package/dist/components/tree/clickMap.js +33 -0
  61. package/dist/components/tree/clickMap.js.map +1 -0
  62. package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
  63. package/dist/components/tree/renderTreeCanvas.js +8 -7
  64. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  65. package/dist/constants.d.ts +1 -0
  66. package/dist/constants.js +1 -0
  67. package/dist/constants.js.map +1 -1
  68. package/dist/fetchUtils.d.ts +2 -1
  69. package/dist/fetchUtils.js +3 -2
  70. package/dist/fetchUtils.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/model/msaModel.d.ts +25 -2
  73. package/dist/model/msaModel.js +15 -3
  74. package/dist/model/msaModel.js.map +1 -1
  75. package/dist/model/treeModel.d.ts +46 -5
  76. package/dist/model/treeModel.js +20 -7
  77. package/dist/model/treeModel.js.map +1 -1
  78. package/dist/model.d.ts +545 -132
  79. package/dist/model.js +101 -75
  80. package/dist/model.js.map +1 -1
  81. package/dist/propertyConservation.d.ts +7 -0
  82. package/dist/propertyConservation.js +66 -0
  83. package/dist/propertyConservation.js.map +1 -0
  84. package/dist/types.d.ts +4 -1
  85. package/dist/useCanvasAutorun.d.ts +2 -0
  86. package/dist/useCanvasAutorun.js +23 -0
  87. package/dist/useCanvasAutorun.js.map +1 -0
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +4 -6
  91. package/src/colorSchemes.docs.test.ts +57 -0
  92. package/src/columnStats.test.ts +94 -0
  93. package/src/components/ConservationTrack.tsx +17 -15
  94. package/src/components/MSAViewer.tsx +12 -0
  95. package/src/components/SequenceTextArea.tsx +1 -1
  96. package/src/components/TextTrack.tsx +7 -11
  97. package/src/components/Track.tsx +1 -1
  98. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  99. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  100. package/src/components/header/FileMenu.tsx +1 -15
  101. package/src/components/header/Header.tsx +11 -8
  102. package/src/components/header/HeaderInfoArea.tsx +1 -1
  103. package/src/components/header/HeaderStatusArea.tsx +1 -1
  104. package/src/components/header/MSASettingsMenu.tsx +1 -0
  105. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  106. package/src/components/header/ZoomControls.tsx +24 -17
  107. package/src/components/header/ZoomMenu.tsx +1 -10
  108. package/src/components/header/settingsMenuItems.ts +9 -1
  109. package/src/components/msa/ColumnStats.tsx +54 -0
  110. package/src/components/msa/DomainLegend.tsx +5 -1
  111. package/src/components/msa/MSACanvasBlock.tsx +29 -26
  112. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
  113. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +3 -1
  114. package/src/components/msa/renderMSAMouseover.ts +2 -2
  115. package/src/components/overlayColors.ts +12 -0
  116. package/src/components/tracks/renderTracksSvg.ts +22 -8
  117. package/src/components/tree/TreeCanvas.tsx +47 -54
  118. package/src/components/tree/TreeCanvasBlock.tsx +10 -68
  119. package/src/components/tree/clickMap.ts +55 -0
  120. package/src/components/tree/renderTreeCanvas.ts +10 -34
  121. package/src/constants.ts +1 -0
  122. package/src/fetchUtils.ts +6 -3
  123. package/src/index.ts +1 -1
  124. package/src/model/msaModel.ts +24 -3
  125. package/src/model/treeModel.ts +27 -7
  126. package/src/model.ts +122 -117
  127. package/src/propertyConservation.test.ts +47 -0
  128. package/src/propertyConservation.ts +73 -0
  129. package/src/stripDefaultSnapshot.test.ts +57 -0
  130. package/src/types.ts +7 -1
  131. package/src/useCanvasAutorun.ts +28 -0
  132. package/src/version.ts +1 -1
  133. package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
  134. package/dist/components/dialogs/SettingsDialog.js +0 -100
  135. package/dist/components/dialogs/SettingsDialog.js.map +0 -1
  136. package/dist/components/header/HeaderMenu.js +0 -27
  137. package/dist/components/header/HeaderMenu.js.map +0 -1
  138. package/src/components/dialogs/SettingsDialog.tsx +0 -234
  139. package/src/components/header/HeaderMenu.tsx +0 -38
package/src/fetchUtils.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { fetchAndMaybeUnzipText } from '@jbrowse/core/util'
1
+ import { fetchAndMaybeUnzipText, statusMessageText } from '@jbrowse/core/util'
2
+
3
+ import type { RpcStatus } from '@jbrowse/core/util'
2
4
 
3
5
  export interface FetchStatus {
4
6
  msg: string
@@ -10,7 +12,7 @@ type Filehandle = Parameters<typeof fetchAndMaybeUnzipText>[0]
10
12
 
11
13
  type ProgressFetcher = (
12
14
  loc: Filehandle,
13
- opts: { signal: AbortSignal; statusCallback: (msg: string) => void },
15
+ opts: { signal: AbortSignal; statusCallback: (status: RpcStatus) => void },
14
16
  ) => Promise<string>
15
17
 
16
18
  /**
@@ -28,7 +30,8 @@ export async function fetchTextWithProgress(
28
30
  try {
29
31
  return await fetcher(loc, {
30
32
  signal: controller.signal,
31
- statusCallback: msg => {
33
+ statusCallback: status => {
34
+ const msg = statusMessageText(status)
32
35
  setStatus(
33
36
  msg
34
37
  ? {
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ export { renderToSvg } from './renderToSvg.tsx'
9
9
  export { default as MSAView } from './components/Loading.tsx'
10
10
  export { default as MSAViewer } from './components/MSAViewer.tsx'
11
11
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
12
- export type { MSAParserType } from 'msa-parsers'
12
+ export type { MSAFormat, MSAParserType } from 'msa-parsers'
13
13
  export type { HierarchyNode } from './hierarchy.ts'
14
14
  export type { InterProScanResults } from './launchInterProScan.ts'
15
15
  export type {
@@ -1,6 +1,10 @@
1
1
  import { types } from '@jbrowse/mobx-state-tree'
2
2
 
3
- import { defaultBgColor, defaultColorSchemeName } from '../constants.ts'
3
+ import {
4
+ defaultBgColor,
5
+ defaultColorSchemeName,
6
+ defaultShowColumnStats,
7
+ } from '../constants.ts'
4
8
 
5
9
  import type { MSAFormat } from 'msa-parsers'
6
10
 
@@ -16,13 +20,23 @@ export function MSAModelF() {
16
20
  * #property
17
21
  * draw MSA tiles with a background color
18
22
  */
19
- bgColor: defaultBgColor,
23
+ bgColor: types.stripDefault(types.boolean, defaultBgColor),
20
24
 
21
25
  /**
22
26
  * #property
23
27
  * default color scheme name
24
28
  */
25
- colorSchemeName: defaultColorSchemeName,
29
+ colorSchemeName: types.stripDefault(types.string, defaultColorSchemeName),
30
+
31
+ /**
32
+ * #property
33
+ * show a per-column statistics tooltip (consensus, conservation, gaps,
34
+ * residue distribution) while hovering the alignment
35
+ */
36
+ showColumnStats: types.stripDefault(
37
+ types.boolean,
38
+ defaultShowColumnStats,
39
+ ),
26
40
 
27
41
  /**
28
42
  * #property
@@ -49,6 +63,13 @@ export function MSAModelF() {
49
63
  self.bgColor = arg
50
64
  },
51
65
 
66
+ /**
67
+ * #action
68
+ */
69
+ setShowColumnStats(arg: boolean) {
70
+ self.showColumnStats = arg
71
+ },
72
+
52
73
  /**
53
74
  * #action
54
75
  * force a specific MSA parser, or pass undefined to auto-detect
@@ -19,25 +19,28 @@ 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(
28
+ types.boolean,
29
+ defaultLabelsAlignRight,
30
+ ),
28
31
 
29
32
  /**
30
33
  * #property
31
34
  * width of the area the tree is drawn in, px
32
35
  */
33
- treeAreaWidth: types.optional(types.number, defaultTreeAreaWidth),
36
+ treeAreaWidth: types.stripDefault(types.number, defaultTreeAreaWidth),
34
37
 
35
38
  /**
36
39
  * #property
37
40
  * width of the tree within the treeArea, px. automatically synced to
38
41
  * fit within treeAreaWidth
39
42
  */
40
- treeWidth: types.optional(types.number, defaultTreeWidth),
43
+ treeWidth: types.stripDefault(types.number, defaultTreeWidth),
41
44
 
42
45
  /**
43
46
  * #property
@@ -45,19 +48,30 @@ export function TreeModelF() {
45
48
  * lengths. if false, the layout is a "cladogram" that does not take into
46
49
  * account evolutionary distances
47
50
  */
48
- showBranchLen: defaultShowBranchLen,
51
+ showBranchLen: types.stripDefault(types.boolean, defaultShowBranchLen),
49
52
 
50
53
  /**
51
54
  * #property
52
55
  * draw tree, boolean
53
56
  */
54
- drawTree: defaultDrawTree,
57
+ drawTree: types.stripDefault(types.boolean, defaultDrawTree),
55
58
 
56
59
  /**
57
60
  * #property
58
61
  * draw clickable node bubbles on the tree
59
62
  */
60
- drawNodeBubbles: defaultDrawNodeBubbles,
63
+ drawNodeBubbles: types.stripDefault(
64
+ types.boolean,
65
+ defaultDrawNodeBubbles,
66
+ ),
67
+
68
+ /**
69
+ * #property
70
+ * auto-size treeAreaWidth to fit the row labels (plus the tree, if drawn)
71
+ * instead of using a fixed width. useful when there is no tree, so the
72
+ * label gutter isn't padded out to the default 400px
73
+ */
74
+ autoTreeAreaWidth: types.stripDefault(types.boolean, false),
61
75
  })
62
76
  .actions(self => ({
63
77
  /**
@@ -87,6 +101,12 @@ export function TreeModelF() {
87
101
  setDrawTree(arg: boolean) {
88
102
  self.drawTree = arg
89
103
  },
104
+ /**
105
+ * #action
106
+ */
107
+ setAutoTreeAreaWidth(arg: boolean) {
108
+ self.autoTreeAreaWidth = arg
109
+ },
90
110
 
91
111
  /**
92
112
  * #action
package/src/model.ts CHANGED
@@ -20,26 +20,18 @@ 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,
@@ -72,6 +64,7 @@ import { MSAModelF } from './model/msaModel.ts'
72
64
  import { TreeModelF } from './model/treeModel.ts'
73
65
  import { calculateNeighborJoiningTree } from './neighborJoining.ts'
74
66
  import { parseAsn1 } from './parseAsn1.ts'
67
+ import { calculatePropertyConservation } from './propertyConservation.ts'
75
68
  import {
76
69
  globalColToVisibleCol,
77
70
  visibleColToGlobalCol,
@@ -131,19 +124,25 @@ function stateModelFactory() {
131
124
  /**
132
125
  * #property
133
126
  */
134
- showDomains: defaultShowDomains,
127
+ showDomains: types.stripDefault(types.boolean, defaultShowDomains),
135
128
  /**
136
129
  * #property
137
130
  */
138
- hideGaps: defaultHideGaps,
131
+ hideGaps: types.stripDefault(types.boolean, defaultHideGaps),
139
132
  /**
140
133
  * #property
141
134
  */
142
- allowedGappyness: defaultAllowedGappyness,
135
+ allowedGappyness: types.stripDefault(
136
+ types.number,
137
+ defaultAllowedGappyness,
138
+ ),
143
139
  /**
144
140
  * #property
145
141
  */
146
- subFeatureRows: defaultSubFeatureRows,
142
+ subFeatureRows: types.stripDefault(
143
+ types.boolean,
144
+ defaultSubFeatureRows,
145
+ ),
147
146
 
148
147
  /**
149
148
  * #property
@@ -154,43 +153,46 @@ function stateModelFactory() {
154
153
  /**
155
154
  * #property
156
155
  */
157
- drawMsaLetters: defaultDrawMsaLetters,
156
+ drawMsaLetters: types.stripDefault(
157
+ types.boolean,
158
+ defaultDrawMsaLetters,
159
+ ),
158
160
 
159
161
  /**
160
162
  * #property
161
163
  * zoom in/out on plain mouse-wheel without holding ctrl
162
164
  */
163
- scrollZoom: defaultScrollZoom,
165
+ scrollZoom: types.stripDefault(types.boolean, defaultScrollZoom),
164
166
 
165
167
  /**
166
168
  * #property
167
169
  * height of the div containing the view, px
168
170
  */
169
- height: types.optional(types.number, defaultHeight),
171
+ height: types.stripDefault(types.number, defaultHeight),
170
172
 
171
173
  /**
172
174
  * #property
173
175
  * height of each row, px
174
176
  */
175
- rowHeight: defaultRowHeight,
177
+ rowHeight: types.stripDefault(types.number, defaultRowHeight),
176
178
 
177
179
  /**
178
180
  * #property
179
181
  * scroll position, Y-offset, px
180
182
  */
181
- scrollY: defaultScrollY,
183
+ scrollY: types.stripDefault(types.number, defaultScrollY),
182
184
 
183
185
  /**
184
186
  * #property
185
187
  * scroll position, X-offset, px
186
188
  */
187
- scrollX: defaultScrollX,
189
+ scrollX: types.stripDefault(types.number, defaultScrollX),
188
190
 
189
191
  /**
190
192
  * #property
191
193
  * width of columns, px
192
194
  */
193
- colWidth: defaultColWidth,
195
+ colWidth: types.stripDefault(types.number, defaultColWidth),
194
196
 
195
197
  /**
196
198
  * #property
@@ -220,14 +222,17 @@ function stateModelFactory() {
220
222
  /**
221
223
  * #property
222
224
  */
223
- currentAlignment: defaultCurrentAlignment,
225
+ currentAlignment: types.stripDefault(
226
+ types.number,
227
+ defaultCurrentAlignment,
228
+ ),
224
229
 
225
230
  /**
226
231
  * #property
227
232
  * array of tree parent nodes that are 'collapsed' (all children are
228
233
  * hidden)
229
234
  */
230
- collapsed: types.array(types.string),
235
+ collapsed: types.stripDefault(types.array(types.string), []),
231
236
 
232
237
  /**
233
238
  * #property
@@ -238,7 +243,7 @@ function stateModelFactory() {
238
243
  * #property
239
244
  * turned off tracks
240
245
  */
241
- turnedOffTracks: types.map(types.boolean),
246
+ turnedOffTracks: types.stripDefault(types.map(types.boolean), {}),
242
247
 
243
248
  /**
244
249
  * #property
@@ -254,7 +259,7 @@ function stateModelFactory() {
254
259
  /**
255
260
  * #property
256
261
  */
257
- featureFilters: types.map(types.boolean),
262
+ featureFilters: types.stripDefault(types.map(types.boolean), {}),
258
263
  /**
259
264
  * #property
260
265
  */
@@ -279,8 +284,7 @@ function stateModelFactory() {
279
284
  * #volatile
280
285
  */
281
286
  status: undefined as
282
- | { msg: string; url?: string; onCancel?: () => void }
283
- | undefined,
287
+ { msg: string; url?: string; onCancel?: () => void } | undefined,
284
288
  /**
285
289
  * #volatile
286
290
  * high resolution scale factor, helps make canvas look better on hi-dpi
@@ -343,8 +347,7 @@ function stateModelFactory() {
343
347
  * the currently hovered tree node ID and its descendant leaf names
344
348
  */
345
349
  hoveredTreeNode: undefined as
346
- | { nodeId: string; descendantNames: string[] }
347
- | undefined,
350
+ { nodeId: string; descendantNames: string[] } | undefined,
348
351
 
349
352
  /**
350
353
  * #volatile
@@ -377,8 +380,7 @@ function stateModelFactory() {
377
380
  * #volatile
378
381
  */
379
382
  interProAnnotations: undefined as
380
- | undefined
381
- | Record<string, InterProScanResults>,
383
+ undefined | Record<string, InterProScanResults>,
382
384
  }))
383
385
  .actions(self => ({
384
386
  /**
@@ -1044,6 +1046,17 @@ function stateModelFactory() {
1044
1046
  return conservation * (1 - gapFraction)
1045
1047
  })
1046
1048
  },
1049
+ /**
1050
+ * #getter
1051
+ * Per-column conservation of physicochemical property class (amino acids
1052
+ * only). Surfaces conservative-substitution sites that identity-based
1053
+ * conservation misses. Empty for nucleotide alignments.
1054
+ */
1055
+ get propertyConservation() {
1056
+ return this.sequenceType === 'amino'
1057
+ ? calculatePropertyConservation(this.colStats, this.colStatsSums)
1058
+ : []
1059
+ },
1047
1060
  /**
1048
1061
  * #getter
1049
1062
  * generates a new tree that is clustered with x,y positions
@@ -1466,10 +1479,24 @@ function stateModelFactory() {
1466
1479
  id: 'conservation',
1467
1480
  name: 'Conservation',
1468
1481
  height: self.conservationTrackHeight,
1482
+ barColor: 'gray',
1469
1483
  },
1470
1484
  ReactComponent: ConservationTrack,
1471
1485
  }
1472
- return [...this.adapterTrackModels, conservationTrack]
1486
+ const propertyConservationTrack: BasicTrack = {
1487
+ model: {
1488
+ id: 'property-conservation',
1489
+ name: 'Property conservation',
1490
+ height: self.conservationTrackHeight,
1491
+ barColor: '#6a51a3',
1492
+ },
1493
+ ReactComponent: ConservationTrack,
1494
+ }
1495
+ return [
1496
+ ...this.adapterTrackModels,
1497
+ conservationTrack,
1498
+ ...(self.sequenceType === 'amino' ? [propertyConservationTrack] : []),
1499
+ ]
1473
1500
  },
1474
1501
 
1475
1502
  /**
@@ -1743,6 +1770,41 @@ function stateModelFactory() {
1743
1770
  return []
1744
1771
  },
1745
1772
 
1773
+ /**
1774
+ * #getter
1775
+ * per-column summary statistics for the hovered column: consensus residue
1776
+ * and its identity fraction, conservation score, gap fraction, and the
1777
+ * sorted non-gap residue distribution. undefined when nothing is hovered.
1778
+ */
1779
+ get mouseOverColumnStats() {
1780
+ const { mouseCol } = self
1781
+ if (mouseCol === undefined) {
1782
+ return undefined
1783
+ }
1784
+ const stats = self.colStats[mouseCol]
1785
+ const total = self.colStatsSums[mouseCol]
1786
+ if (!stats || !total) {
1787
+ return undefined
1788
+ }
1789
+ const gaps = (stats['-'] ?? 0) + (stats['.'] ?? 0)
1790
+ const distribution = Object.entries(stats)
1791
+ .filter(([letter]) => letter !== '-' && letter !== '.')
1792
+ .sort((a, b) => b[1] - a[1])
1793
+ const consensus = distribution[0]
1794
+ return {
1795
+ col: mouseCol,
1796
+ total,
1797
+ gaps,
1798
+ gapFraction: gaps / total,
1799
+ conservation: self.conservation[mouseCol] ?? 0,
1800
+ propertyConservation: self.propertyConservation[mouseCol],
1801
+ consensusLetter: consensus?.[0] ?? '',
1802
+ consensusCount: consensus?.[1] ?? 0,
1803
+ consensusFraction: consensus ? consensus[1] / total : 0,
1804
+ distribution,
1805
+ }
1806
+ },
1807
+
1746
1808
  /**
1747
1809
  * #method
1748
1810
  */
@@ -2046,6 +2108,23 @@ function stateModelFactory() {
2046
2108
  }),
2047
2109
  )
2048
2110
 
2111
+ // autorun: when autoTreeAreaWidth is set and no tree is drawn, shrink the
2112
+ // tree area to fit the row labels rather than padding it to the fixed
2113
+ // default. Gated on noTree/!drawTree so it never fights the treeWidth sync
2114
+ // below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
2115
+ addDisposer(
2116
+ self,
2117
+ autorun(() => {
2118
+ if (
2119
+ self.autoTreeAreaWidth &&
2120
+ (self.noTree || !self.drawTree) &&
2121
+ self.labelsWidth
2122
+ ) {
2123
+ self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12)
2124
+ }
2125
+ }),
2126
+ )
2127
+
2049
2128
  // autorun synchronizes treeWidth with treeAreaWidth
2050
2129
  addDisposer(
2051
2130
  self,
@@ -2060,93 +2139,19 @@ function stateModelFactory() {
2060
2139
  )
2061
2140
  },
2062
2141
  }))
2063
- .postProcessSnapshot(result => {
2064
- const snap = result as Omit<typeof result, symbol>
2065
- const {
2066
- data: { tree, msa, treeMetadata },
2067
- // Main model properties
2068
- showDomains,
2069
- hideGaps,
2070
- allowedGappyness,
2071
- subFeatureRows,
2072
- drawMsaLetters,
2073
- height,
2074
- rowHeight,
2075
- scrollY,
2076
- scrollX,
2077
- colWidth,
2078
- currentAlignment,
2079
- collapsed,
2080
- showOnly,
2081
- turnedOffTracks,
2082
- featureFilters,
2083
- relativeTo,
2084
- // MSA model properties
2085
- bgColor,
2086
- colorSchemeName,
2087
- // Tree model properties
2088
- drawLabels,
2089
- labelsAlignRight,
2090
- treeAreaWidth,
2091
- treeWidth,
2092
- showBranchLen,
2093
- drawTree,
2094
- drawNodeBubbles,
2095
- // Always include
2096
- ...rest
2097
- } = snap
2098
-
2099
- const defaults: Record<string, unknown> = {
2100
- showDomains: defaultShowDomains,
2101
- hideGaps: defaultHideGaps,
2102
- allowedGappyness: defaultAllowedGappyness,
2103
- subFeatureRows: defaultSubFeatureRows,
2104
- drawMsaLetters: defaultDrawMsaLetters,
2105
- height: defaultHeight,
2106
- rowHeight: defaultRowHeight,
2107
- scrollY: defaultScrollY,
2108
- scrollX: defaultScrollX,
2109
- colWidth: defaultColWidth,
2110
- currentAlignment: defaultCurrentAlignment,
2111
- bgColor: defaultBgColor,
2112
- colorSchemeName: defaultColorSchemeName,
2113
- drawLabels: defaultDrawLabels,
2114
- labelsAlignRight: defaultLabelsAlignRight,
2115
- treeAreaWidth: defaultTreeAreaWidth,
2116
- treeWidth: defaultTreeWidth,
2117
- showBranchLen: defaultShowBranchLen,
2118
- drawTree: defaultDrawTree,
2119
- drawNodeBubbles: defaultDrawNodeBubbles,
2120
- }
2121
-
2122
- const nonDefaults = Object.fromEntries(
2123
- Object.entries(defaults)
2124
- .filter(([key, def]) => snap[key as keyof typeof snap] !== def)
2125
- .map(([key]) => [key, snap[key as keyof typeof snap]]),
2126
- )
2127
-
2128
- return {
2129
- ...rest,
2130
- data: {
2131
- ...(result.treeFilehandle ? {} : { tree }),
2132
- ...(result.msaFilehandle ? {} : { msa }),
2133
- ...(result.treeMetadataFilehandle ? {} : { treeMetadata }),
2134
- },
2135
- ...nonDefaults,
2136
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2137
- ...(collapsed?.length ? { collapsed } : {}),
2138
- ...(showOnly !== undefined ? { showOnly } : {}),
2139
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2140
- ...(turnedOffTracks && Object.keys(turnedOffTracks).length > 0
2141
- ? { turnedOffTracks }
2142
- : {}),
2143
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2144
- ...(featureFilters && Object.keys(featureFilters).length > 0
2145
- ? { featureFilters }
2146
- : {}),
2147
- ...(relativeTo !== undefined ? { relativeTo } : {}),
2148
- } as typeof snap
2149
- })
2142
+ .postProcessSnapshot(({ data, ...rest }) => ({
2143
+ // per-property defaults are stripped by types.stripDefault; the only thing
2144
+ // it can't express is this cross-field rule: drop inline tree/msa/metadata
2145
+ // when a sibling filehandle can refetch them, keeping sessions/URLs small
2146
+ ...rest,
2147
+ data: {
2148
+ ...(rest.treeFilehandle ? {} : { tree: data.tree }),
2149
+ ...(rest.msaFilehandle ? {} : { msa: data.msa }),
2150
+ ...(rest.treeMetadataFilehandle
2151
+ ? {}
2152
+ : { treeMetadata: data.treeMetadata }),
2153
+ },
2154
+ }))
2150
2155
  }
2151
2156
 
2152
2157
  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
+ })
@@ -0,0 +1,73 @@
1
+ // Disjoint physicochemical classes for the 20 standard amino acids. Property
2
+ // conservation asks a different question than identity conservation: a column
3
+ // can vary in exact residue yet stay within one property class (a conservative
4
+ // substitution), which often marks a functionally constrained site that plain
5
+ // identity conservation reports as variable.
6
+ //
7
+ // Histidine is grouped with the aromatics (it is only weakly/conditionally
8
+ // positive at physiological pH); this mirrors common substitution-class schemes.
9
+ const aminoPropertyClass: Record<string, string> = {
10
+ A: 'hydrophobic',
11
+ V: 'hydrophobic',
12
+ L: 'hydrophobic',
13
+ I: 'hydrophobic',
14
+ M: 'hydrophobic',
15
+ C: 'hydrophobic',
16
+ F: 'aromatic',
17
+ W: 'aromatic',
18
+ Y: 'aromatic',
19
+ H: 'aromatic',
20
+ K: 'positive',
21
+ R: 'positive',
22
+ D: 'negative',
23
+ E: 'negative',
24
+ S: 'polar',
25
+ T: 'polar',
26
+ N: 'polar',
27
+ Q: 'polar',
28
+ G: 'special',
29
+ P: 'special',
30
+ }
31
+
32
+ const numPropertyClasses = new Set(Object.values(aminoPropertyClass)).size
33
+ const maxPropertyEntropy = Math.log2(numPropertyClasses)
34
+
35
+ /**
36
+ * Per-column property conservation using Shannon entropy over physicochemical
37
+ * classes rather than exact residues. Returns 0-1 where 1 = every non-gap
38
+ * residue shares one property class. Mirrors the identity-conservation formula
39
+ * so the two tracks are directly comparable: (1 - H/Hmax) * (1 - gapFraction).
40
+ */
41
+ export function calculatePropertyConservation(
42
+ colStats: Record<string, number>[],
43
+ colStatsSums: number[],
44
+ ) {
45
+ return colStats.map((stats, i) => {
46
+ const total = colStatsSums[i]
47
+ if (!total) {
48
+ return 0
49
+ }
50
+
51
+ let nonGapTotal = 0
52
+ const classCounts: Record<string, number> = {}
53
+ for (const [letter, count] of Object.entries(stats)) {
54
+ const cls = aminoPropertyClass[letter]
55
+ if (cls) {
56
+ classCounts[cls] = (classCounts[cls] ?? 0) + count
57
+ nonGapTotal += count
58
+ }
59
+ }
60
+ if (nonGapTotal === 0) {
61
+ return 0
62
+ }
63
+
64
+ let entropy = 0
65
+ for (const count of Object.values(classCounts)) {
66
+ const freq = count / nonGapTotal
67
+ entropy -= freq * Math.log2(freq)
68
+ }
69
+
70
+ const gapFraction = (total - nonGapTotal) / total
71
+ return Math.max(0, 1 - entropy / maxPropertyEntropy) * (1 - gapFraction)
72
+ })
73
+ }