react-msaview 5.5.0 → 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 (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 +55 -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 +21 -3
  125. package/src/model/treeModel.ts +21 -7
  126. package/src/model.ts +116 -117
  127. package/src/propertyConservation.test.ts +47 -0
  128. package/src/propertyConservation.ts +73 -0
  129. package/src/stripDefaultSnapshot.test.ts +54 -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,20 @@ 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(types.boolean, defaultShowColumnStats),
26
37
 
27
38
  /**
28
39
  * #property
@@ -49,6 +60,13 @@ export function MSAModelF() {
49
60
  self.bgColor = arg
50
61
  },
51
62
 
63
+ /**
64
+ * #action
65
+ */
66
+ setShowColumnStats(arg: boolean) {
67
+ self.showColumnStats = arg
68
+ },
69
+
52
70
  /**
53
71
  * #action
54
72
  * force a specific MSA parser, or pass undefined to auto-detect
@@ -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,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,22 @@ 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(types.boolean, defaultSubFeatureRows),
147
143
 
148
144
  /**
149
145
  * #property
@@ -154,43 +150,43 @@ function stateModelFactory() {
154
150
  /**
155
151
  * #property
156
152
  */
157
- drawMsaLetters: defaultDrawMsaLetters,
153
+ drawMsaLetters: types.stripDefault(types.boolean, defaultDrawMsaLetters),
158
154
 
159
155
  /**
160
156
  * #property
161
157
  * zoom in/out on plain mouse-wheel without holding ctrl
162
158
  */
163
- scrollZoom: defaultScrollZoom,
159
+ scrollZoom: types.stripDefault(types.boolean, defaultScrollZoom),
164
160
 
165
161
  /**
166
162
  * #property
167
163
  * height of the div containing the view, px
168
164
  */
169
- height: types.optional(types.number, defaultHeight),
165
+ height: types.stripDefault(types.number, defaultHeight),
170
166
 
171
167
  /**
172
168
  * #property
173
169
  * height of each row, px
174
170
  */
175
- rowHeight: defaultRowHeight,
171
+ rowHeight: types.stripDefault(types.number, defaultRowHeight),
176
172
 
177
173
  /**
178
174
  * #property
179
175
  * scroll position, Y-offset, px
180
176
  */
181
- scrollY: defaultScrollY,
177
+ scrollY: types.stripDefault(types.number, defaultScrollY),
182
178
 
183
179
  /**
184
180
  * #property
185
181
  * scroll position, X-offset, px
186
182
  */
187
- scrollX: defaultScrollX,
183
+ scrollX: types.stripDefault(types.number, defaultScrollX),
188
184
 
189
185
  /**
190
186
  * #property
191
187
  * width of columns, px
192
188
  */
193
- colWidth: defaultColWidth,
189
+ colWidth: types.stripDefault(types.number, defaultColWidth),
194
190
 
195
191
  /**
196
192
  * #property
@@ -220,14 +216,17 @@ function stateModelFactory() {
220
216
  /**
221
217
  * #property
222
218
  */
223
- currentAlignment: defaultCurrentAlignment,
219
+ currentAlignment: types.stripDefault(
220
+ types.number,
221
+ defaultCurrentAlignment,
222
+ ),
224
223
 
225
224
  /**
226
225
  * #property
227
226
  * array of tree parent nodes that are 'collapsed' (all children are
228
227
  * hidden)
229
228
  */
230
- collapsed: types.array(types.string),
229
+ collapsed: types.stripDefault(types.array(types.string), []),
231
230
 
232
231
  /**
233
232
  * #property
@@ -238,7 +237,7 @@ function stateModelFactory() {
238
237
  * #property
239
238
  * turned off tracks
240
239
  */
241
- turnedOffTracks: types.map(types.boolean),
240
+ turnedOffTracks: types.stripDefault(types.map(types.boolean), {}),
242
241
 
243
242
  /**
244
243
  * #property
@@ -254,7 +253,7 @@ function stateModelFactory() {
254
253
  /**
255
254
  * #property
256
255
  */
257
- featureFilters: types.map(types.boolean),
256
+ featureFilters: types.stripDefault(types.map(types.boolean), {}),
258
257
  /**
259
258
  * #property
260
259
  */
@@ -279,8 +278,7 @@ function stateModelFactory() {
279
278
  * #volatile
280
279
  */
281
280
  status: undefined as
282
- | { msg: string; url?: string; onCancel?: () => void }
283
- | undefined,
281
+ { msg: string; url?: string; onCancel?: () => void } | undefined,
284
282
  /**
285
283
  * #volatile
286
284
  * high resolution scale factor, helps make canvas look better on hi-dpi
@@ -343,8 +341,7 @@ function stateModelFactory() {
343
341
  * the currently hovered tree node ID and its descendant leaf names
344
342
  */
345
343
  hoveredTreeNode: undefined as
346
- | { nodeId: string; descendantNames: string[] }
347
- | undefined,
344
+ { nodeId: string; descendantNames: string[] } | undefined,
348
345
 
349
346
  /**
350
347
  * #volatile
@@ -377,8 +374,7 @@ function stateModelFactory() {
377
374
  * #volatile
378
375
  */
379
376
  interProAnnotations: undefined as
380
- | undefined
381
- | Record<string, InterProScanResults>,
377
+ undefined | Record<string, InterProScanResults>,
382
378
  }))
383
379
  .actions(self => ({
384
380
  /**
@@ -1044,6 +1040,17 @@ function stateModelFactory() {
1044
1040
  return conservation * (1 - gapFraction)
1045
1041
  })
1046
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
+ },
1047
1054
  /**
1048
1055
  * #getter
1049
1056
  * generates a new tree that is clustered with x,y positions
@@ -1466,10 +1473,24 @@ function stateModelFactory() {
1466
1473
  id: 'conservation',
1467
1474
  name: 'Conservation',
1468
1475
  height: self.conservationTrackHeight,
1476
+ barColor: 'gray',
1469
1477
  },
1470
1478
  ReactComponent: ConservationTrack,
1471
1479
  }
1472
- 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
+ ]
1473
1494
  },
1474
1495
 
1475
1496
  /**
@@ -1743,6 +1764,41 @@ function stateModelFactory() {
1743
1764
  return []
1744
1765
  },
1745
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
+
1746
1802
  /**
1747
1803
  * #method
1748
1804
  */
@@ -2046,6 +2102,23 @@ function stateModelFactory() {
2046
2102
  }),
2047
2103
  )
2048
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
+
2049
2122
  // autorun synchronizes treeWidth with treeAreaWidth
2050
2123
  addDisposer(
2051
2124
  self,
@@ -2060,93 +2133,19 @@ function stateModelFactory() {
2060
2133
  )
2061
2134
  },
2062
2135
  }))
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
- })
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
+ }))
2150
2149
  }
2151
2150
 
2152
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
+ })
@@ -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
+ }