react-msaview 9.1.0 → 9.2.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 (278) hide show
  1. package/bundle/index.js +111 -115
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blosum62.d.ts +3 -0
  4. package/dist/blosum62.js +43 -0
  5. package/dist/blosum62.js.map +1 -0
  6. package/dist/colorSchemes.js +1 -0
  7. package/dist/colorSchemes.js.map +1 -1
  8. package/dist/components/Loading.d.ts +3 -2
  9. package/dist/components/Loading.js +2 -2
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAView.d.ts +3 -2
  12. package/dist/components/MSAView.js +7 -4
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/MSAViewer.d.ts +64 -12
  15. package/dist/components/MetadataTable.d.ts +4 -1
  16. package/dist/components/MetadataTable.js +55 -16
  17. package/dist/components/MetadataTable.js.map +1 -1
  18. package/dist/components/Track.js +6 -0
  19. package/dist/components/Track.js.map +1 -1
  20. package/dist/components/controls.js +2 -1
  21. package/dist/components/controls.js.map +1 -1
  22. package/dist/components/dialogs/AnnotationFileDialog.js +7 -2
  23. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  24. package/dist/components/dialogs/FeatureDialog.js +2 -2
  25. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  26. package/dist/components/{header/MSASettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
  27. package/dist/components/dialogs/GapThresholdDialog.js +15 -0
  28. package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
  29. package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
  30. package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
  31. package/dist/components/dialogs/GappynessSlider.js.map +1 -0
  32. package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
  33. package/dist/components/dialogs/GoToBox.js +48 -0
  34. package/dist/components/dialogs/GoToBox.js.map +1 -0
  35. package/dist/components/dialogs/GoToDialog.d.ts +6 -0
  36. package/dist/components/dialogs/GoToDialog.js +10 -0
  37. package/dist/components/dialogs/GoToDialog.js.map +1 -0
  38. package/dist/components/dialogs/MetadataDialog.js +11 -3
  39. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  40. package/dist/components/dialogs/goTo.d.ts +5 -0
  41. package/dist/components/{header → dialogs}/goTo.js +1 -1
  42. package/dist/components/dialogs/goTo.js.map +1 -0
  43. package/dist/components/dialogs/metadataLabels.d.ts +4 -0
  44. package/dist/components/dialogs/metadataLabels.js +52 -0
  45. package/dist/components/dialogs/metadataLabels.js.map +1 -0
  46. package/dist/components/dialogs/metadataRecord.d.ts +1 -0
  47. package/dist/components/dialogs/metadataRecord.js +38 -0
  48. package/dist/components/dialogs/metadataRecord.js.map +1 -0
  49. package/dist/components/header/Header.d.ts +6 -2
  50. package/dist/components/header/Header.js +30 -32
  51. package/dist/components/header/Header.js.map +1 -1
  52. package/dist/components/header/{TreeSettingsMenu.d.ts → HiddenColumnsChip.d.ts} +2 -2
  53. package/dist/components/header/HiddenColumnsChip.js +15 -0
  54. package/dist/components/header/HiddenColumnsChip.js.map +1 -0
  55. package/dist/components/header/MenuBar.d.ts +6 -0
  56. package/dist/components/header/MenuBar.js +33 -0
  57. package/dist/components/header/MenuBar.js.map +1 -0
  58. package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
  59. package/dist/components/header/ScrollZoomCheckbox.js +19 -0
  60. package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
  61. package/dist/components/header/ZoomControls.js +10 -30
  62. package/dist/components/header/ZoomControls.js.map +1 -1
  63. package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
  64. package/dist/components/header/colorSchemeMenuItems.js +106 -0
  65. package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
  66. package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
  67. package/dist/components/header/getAnnotationsMenu.js +48 -54
  68. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  69. package/dist/components/header/menuBarItems.d.ts +11 -0
  70. package/dist/components/header/menuBarItems.js +277 -0
  71. package/dist/components/header/menuBarItems.js.map +1 -0
  72. package/dist/components/import/ImportForm.js +12 -6
  73. package/dist/components/import/ImportForm.js.map +1 -1
  74. package/dist/components/minimap/Minimap.js +7 -3
  75. package/dist/components/minimap/Minimap.js.map +1 -1
  76. package/dist/components/minimap/MinimapSVG.js +2 -1
  77. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  78. package/dist/components/minimap/minimapLayout.d.ts +12 -0
  79. package/dist/components/minimap/minimapLayout.js +20 -0
  80. package/dist/components/minimap/minimapLayout.js.map +1 -1
  81. package/dist/components/msa/MSACanvas.js +26 -9
  82. package/dist/components/msa/MSACanvas.js.map +1 -1
  83. package/dist/components/msa/MSACanvasBlock.js +15 -11
  84. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  85. package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
  86. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  87. package/dist/components/msa/WheelZoomHint.d.ts +7 -0
  88. package/dist/components/msa/WheelZoomHint.js +38 -0
  89. package/dist/components/msa/WheelZoomHint.js.map +1 -0
  90. package/dist/components/msa/msaKeys.js +3 -0
  91. package/dist/components/msa/msaKeys.js.map +1 -1
  92. package/dist/components/msa/msaRaster.d.ts +4 -5
  93. package/dist/components/msa/msaRaster.js +5 -5
  94. package/dist/components/msa/msaRaster.js.map +1 -1
  95. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +22 -2
  96. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  97. package/dist/components/msa/renderHighlights.d.ts +0 -3
  98. package/dist/components/msa/renderHighlights.js +3 -3
  99. package/dist/components/msa/renderHighlights.js.map +1 -1
  100. package/dist/components/msa/renderMSABlock.d.ts +6 -1
  101. package/dist/components/msa/renderMSABlock.js +44 -17
  102. package/dist/components/msa/renderMSABlock.js.map +1 -1
  103. package/dist/components/msa/tileColor.js +7 -0
  104. package/dist/components/msa/tileColor.js.map +1 -1
  105. package/dist/components/rowpanels/RowPanelBlock.js +2 -2
  106. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  107. package/dist/components/settings/MenuItemsForm.d.ts +10 -0
  108. package/dist/components/settings/MenuItemsForm.js +80 -0
  109. package/dist/components/settings/MenuItemsForm.js.map +1 -0
  110. package/dist/components/settings/SettingsPanel.d.ts +12 -0
  111. package/dist/components/settings/SettingsPanel.js +92 -0
  112. package/dist/components/settings/SettingsPanel.js.map +1 -0
  113. package/dist/components/{header/FileMenu.d.ts → settings/TrackList.d.ts} +2 -2
  114. package/dist/components/settings/TrackList.js +58 -0
  115. package/dist/components/settings/TrackList.js.map +1 -0
  116. package/dist/components/tracks/TrackBlocks.js +2 -2
  117. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  118. package/dist/components/tracks/TrackTooltipContent.js +2 -1
  119. package/dist/components/tracks/TrackTooltipContent.js.map +1 -1
  120. package/dist/components/tracks/drawTracks.js +13 -6
  121. package/dist/components/tracks/drawTracks.js.map +1 -1
  122. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  123. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  124. package/dist/components/tree/TreeMenu.js +4 -6
  125. package/dist/components/tree/TreeMenu.js.map +1 -1
  126. package/dist/components/tree/TreeOverview.js +2 -2
  127. package/dist/components/tree/TreeOverview.js.map +1 -1
  128. package/dist/constants.d.ts +4 -1
  129. package/dist/constants.js +7 -1
  130. package/dist/constants.js.map +1 -1
  131. package/dist/darkReader.d.ts +7 -0
  132. package/dist/darkReader.js +42 -0
  133. package/dist/darkReader.js.map +1 -0
  134. package/dist/element.js +22 -47
  135. package/dist/element.js.map +1 -1
  136. package/dist/featurePanels.d.ts +37 -0
  137. package/dist/featurePanels.js +187 -0
  138. package/dist/featurePanels.js.map +1 -0
  139. package/dist/filehandleLoader.d.ts +41 -0
  140. package/dist/filehandleLoader.js +99 -0
  141. package/dist/filehandleLoader.js.map +1 -0
  142. package/dist/index.d.ts +3 -0
  143. package/dist/index.js +2 -0
  144. package/dist/index.js.map +1 -1
  145. package/dist/model/treeModel.d.ts +9 -0
  146. package/dist/model/treeModel.js +12 -1
  147. package/dist/model/treeModel.js.map +1 -1
  148. package/dist/model.d.ts +148 -32
  149. package/dist/model.js +291 -569
  150. package/dist/model.js.map +1 -1
  151. package/dist/neighborJoining.js +1 -40
  152. package/dist/neighborJoining.js.map +1 -1
  153. package/dist/renderToSvg.js +1 -13
  154. package/dist/renderToSvg.js.map +1 -1
  155. package/dist/rerootTree.d.ts +1 -0
  156. package/dist/rerootTree.js +7 -6
  157. package/dist/rerootTree.js.map +1 -1
  158. package/dist/schemeResidueColor.d.ts +12 -0
  159. package/dist/schemeResidueColor.js +33 -0
  160. package/dist/schemeResidueColor.js.map +1 -0
  161. package/dist/similarityColors.d.ts +11 -0
  162. package/dist/similarityColors.js +90 -0
  163. package/dist/similarityColors.js.map +1 -0
  164. package/dist/svgTestUtil.d.ts +64 -12
  165. package/dist/treeDisplay.d.ts +37 -0
  166. package/dist/treeDisplay.js +209 -0
  167. package/dist/treeDisplay.js.map +1 -0
  168. package/dist/types.d.ts +7 -3
  169. package/dist/useWheelScroll.d.ts +3 -1
  170. package/dist/useWheelScroll.js +36 -17
  171. package/dist/useWheelScroll.js.map +1 -1
  172. package/dist/util.d.ts +2 -0
  173. package/dist/util.js +2 -0
  174. package/dist/util.js.map +1 -1
  175. package/dist/version.d.ts +1 -1
  176. package/dist/version.js +1 -1
  177. package/dist/wheelZoom.d.ts +8 -3
  178. package/dist/wheelZoom.js +44 -32
  179. package/dist/wheelZoom.js.map +1 -1
  180. package/dist/zoomSnapshot.d.ts +29 -0
  181. package/dist/zoomSnapshot.js +47 -0
  182. package/dist/zoomSnapshot.js.map +1 -0
  183. package/package.json +11 -11
  184. package/src/blosum62.ts +45 -0
  185. package/src/colorSchemes.ts +1 -0
  186. package/src/components/Loading.tsx +6 -2
  187. package/src/components/MSAView.tsx +22 -5
  188. package/src/components/MetadataTable.tsx +83 -26
  189. package/src/components/Track.tsx +13 -0
  190. package/src/components/controls.ts +2 -1
  191. package/src/components/dialogs/AnnotationFileDialog.tsx +12 -1
  192. package/src/components/dialogs/FeatureDialog.tsx +2 -5
  193. package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
  194. package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
  195. package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
  196. package/src/components/dialogs/GoToDialog.tsx +30 -0
  197. package/src/components/dialogs/MetadataDialog.tsx +32 -4
  198. package/src/components/{header → dialogs}/goTo.ts +1 -1
  199. package/src/components/dialogs/metadataLabels.ts +54 -0
  200. package/src/components/dialogs/metadataRecord.tsx +61 -0
  201. package/src/components/header/Header.tsx +75 -42
  202. package/src/components/header/HiddenColumnsChip.tsx +39 -0
  203. package/src/components/header/MenuBar.tsx +64 -0
  204. package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
  205. package/src/components/header/ZoomControls.tsx +20 -35
  206. package/src/components/header/colorSchemeMenuItems.ts +121 -0
  207. package/src/components/header/getAnnotationsMenu.ts +52 -56
  208. package/src/components/header/menuBarItems.ts +357 -0
  209. package/src/components/import/ImportForm.tsx +19 -7
  210. package/src/components/minimap/Minimap.tsx +11 -3
  211. package/src/components/minimap/MinimapSVG.tsx +17 -1
  212. package/src/components/minimap/minimapLayout.ts +27 -0
  213. package/src/components/msa/MSACanvas.tsx +22 -3
  214. package/src/components/msa/MSACanvasBlock.tsx +18 -11
  215. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
  216. package/src/components/msa/WheelZoomHint.tsx +56 -0
  217. package/src/components/msa/msaKeys.ts +3 -0
  218. package/src/components/msa/msaRaster.ts +6 -8
  219. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +28 -5
  220. package/src/components/msa/renderHighlights.ts +3 -3
  221. package/src/components/msa/renderMSABlock.ts +51 -15
  222. package/src/components/msa/tileColor.ts +7 -0
  223. package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
  224. package/src/components/settings/MenuItemsForm.tsx +225 -0
  225. package/src/components/settings/SettingsPanel.tsx +156 -0
  226. package/src/components/settings/TrackList.tsx +124 -0
  227. package/src/components/tracks/TrackBlocks.tsx +2 -2
  228. package/src/components/tracks/TrackTooltipContent.tsx +2 -1
  229. package/src/components/tracks/drawTracks.ts +15 -7
  230. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  231. package/src/components/tree/TreeMenu.tsx +4 -6
  232. package/src/components/tree/TreeOverview.tsx +2 -2
  233. package/src/constants.ts +9 -1
  234. package/src/darkReader.ts +57 -0
  235. package/src/element.ts +34 -57
  236. package/src/featurePanels.ts +269 -0
  237. package/src/filehandleLoader.ts +136 -0
  238. package/src/index.ts +4 -0
  239. package/src/model/treeModel.ts +13 -0
  240. package/src/model.ts +370 -748
  241. package/src/neighborJoining.ts +1 -42
  242. package/src/renderToSvg.tsx +1 -13
  243. package/src/rerootTree.ts +7 -6
  244. package/src/schemeResidueColor.ts +37 -0
  245. package/src/similarityColors.ts +112 -0
  246. package/src/treeDisplay.ts +278 -0
  247. package/src/types.ts +7 -4
  248. package/src/useWheelScroll.ts +43 -22
  249. package/src/util.ts +3 -0
  250. package/src/version.ts +1 -1
  251. package/src/wheelZoom.ts +55 -33
  252. package/src/zoomSnapshot.ts +80 -0
  253. package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
  254. package/dist/components/header/ColorSchemeMenu.js +0 -37
  255. package/dist/components/header/ColorSchemeMenu.js.map +0 -1
  256. package/dist/components/header/FileMenu.js +0 -92
  257. package/dist/components/header/FileMenu.js.map +0 -1
  258. package/dist/components/header/GappynessSlider.js.map +0 -1
  259. package/dist/components/header/GoToBox.js +0 -31
  260. package/dist/components/header/GoToBox.js.map +0 -1
  261. package/dist/components/header/MSASettingsMenu.js +0 -11
  262. package/dist/components/header/MSASettingsMenu.js.map +0 -1
  263. package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
  264. package/dist/components/header/ScrollZoomIcons.js +0 -35
  265. package/dist/components/header/ScrollZoomIcons.js.map +0 -1
  266. package/dist/components/header/TreeSettingsMenu.js +0 -11
  267. package/dist/components/header/TreeSettingsMenu.js.map +0 -1
  268. package/dist/components/header/goTo.d.ts +0 -20
  269. package/dist/components/header/goTo.js.map +0 -1
  270. package/dist/components/header/settingsMenuItems.d.ts +0 -5
  271. package/dist/components/header/settingsMenuItems.js +0 -171
  272. package/dist/components/header/settingsMenuItems.js.map +0 -1
  273. package/src/components/header/ColorSchemeMenu.tsx +0 -56
  274. package/src/components/header/FileMenu.tsx +0 -107
  275. package/src/components/header/MSASettingsMenu.tsx +0 -26
  276. package/src/components/header/ScrollZoomIcons.tsx +0 -52
  277. package/src/components/header/TreeSettingsMenu.tsx +0 -30
  278. package/src/components/header/settingsMenuItems.ts +0 -216
package/src/model.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util'
2
- import { openLocation } from '@jbrowse/core/util/io'
3
2
  import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
4
3
  import {
5
4
  addDisposer,
6
5
  applySnapshot,
7
6
  cast,
8
7
  getSnapshot,
9
- isAlive,
10
8
  types,
11
9
  } from '@jbrowse/mobx-state-tree'
12
10
  import {
@@ -19,10 +17,8 @@ import {
19
17
  import {
20
18
  generateNodeIds,
21
19
  gffToAnnotations,
22
- parseEmfTree,
23
20
  parseGFF,
24
21
  parseMSA,
25
- parseNewick,
26
22
  } from 'msa-parsers'
27
23
 
28
24
  import { blockFasta } from './blockFasta.ts'
@@ -36,14 +32,11 @@ import { visibleColRange } from './components/msa/visibleColRange.ts'
36
32
  import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
37
33
  import { scaleBarLength } from './components/tree/scaleBar.ts'
38
34
  import {
39
- cladeHighlightAlpha,
40
- cladeHighlightColor,
41
35
  defaultAllowedGappyness,
42
36
  defaultColWidth,
43
37
  defaultColorSchemeName,
44
38
  defaultCurrentAlignment,
45
39
  defaultDrawMsaLetters,
46
- defaultFeaturePanelWidth,
47
40
  defaultHeight,
48
41
  defaultHideGaps,
49
42
  defaultRowHeight,
@@ -72,35 +65,37 @@ import {
72
65
  scrollZoomAxes,
73
66
  segmentFeatureTypes,
74
67
  segmentShades,
68
+ settingsPanelWidth,
75
69
  treeScaleBarHeight,
76
70
  } from './constants.ts'
77
71
  import { createPaletteMap } from './createPaletteMap.ts'
78
72
  import { exportFileName } from './exportFileName.ts'
79
- import { featureField, featureName } from './featureFields.ts'
73
+ import { featureName } from './featureFields.ts'
74
+ import {
75
+ colorByRow,
76
+ featureColorMap,
77
+ featureLabelMap,
78
+ featurePanelSpans,
79
+ resolveFeatureScale,
80
+ rowPanelWidth,
81
+ stringFields,
82
+ } from './featurePanels.ts'
80
83
  import { featuresToAnnotations } from './featuresLayer.ts'
81
- import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
82
- import { flatToTree } from './flatToTree.ts'
84
+ import { installFilehandleLoader } from './filehandleLoader.ts'
83
85
  import {
84
- collapse,
85
86
  find,
86
- forEachDescendant,
87
87
  hierarchy,
88
88
  layoutTree,
89
- leafIndex,
90
89
  leaves,
91
- mrca,
92
90
  nodeCoveringRows,
93
- sort,
94
- sum as hierarchySum,
95
91
  } from './hierarchy.ts'
96
92
  import { DataModelF } from './model/DataModel.ts'
97
93
  import { DialogQueueSessionMixin } from './model/DialogQueue.ts'
98
94
  import { MSAModelF } from './model/msaModel.ts'
99
95
  import { TreeModelF } from './model/treeModel.ts'
100
96
  import { calculateNeighborJoiningTree } from './neighborJoining.ts'
101
- import { parseAsn1 } from './parseAsn1.ts'
102
97
  import { calculatePropertyConservation } from './propertyConservation.ts'
103
- import { midpointRoot, rerootTree } from './rerootTree.ts'
98
+ import { preorder } from './rerootTree.ts'
104
99
  import {
105
100
  globalColToVisibleCol,
106
101
  seqPosOfGlobalCol,
@@ -110,33 +105,43 @@ import {
110
105
  import { resolveScale } from './scales.ts'
111
106
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
112
107
  import { maxBitsFor } from './sequenceLogo.ts'
108
+ import {
109
+ blosum62ColumnColors,
110
+ identityColumnColors,
111
+ } from './similarityColors.ts'
113
112
  import { stripDefault } from './stripDefault.ts'
114
113
  import { treeTips, widestLabel } from './tipLabels.ts'
114
+ import {
115
+ buildTreeRoot,
116
+ outgroupNode,
117
+ parseTreeText,
118
+ resolveClades,
119
+ rootTree,
120
+ tipRange,
121
+ } from './treeDisplay.ts'
115
122
  import {
116
123
  computeRowInsertions,
117
124
  dropBlanks,
118
125
  len,
119
- outlineColor,
126
+ readComputed,
120
127
  skipBlanks,
121
128
  withAlpha,
122
129
  } from './util.ts'
123
130
  import { saveAs } from './vendor/fileSaver.ts'
124
131
  import { writeNewick } from './writeNewick.ts'
125
132
  import { parseWuss } from './wuss.ts'
133
+ import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.ts'
126
134
 
127
135
  import type { ColumnStats } from './columnStats.ts'
128
136
  import type { ScrollZoomAxis } from './constants.ts'
129
137
  import type { HierarchyNode } from './hierarchy.ts'
130
138
  import type { ExportSvgOptions } from './renderToSvg.tsx'
131
- import type { TreeNode } from './rerootTree.ts'
132
- import type { ScaleSpec } from './scales.ts'
133
139
  import type {
134
140
  Annotation,
135
141
  Arc,
136
142
  BasicTrack,
137
143
  Cell,
138
144
  Clade,
139
- CladeMark,
140
145
  ResidueMappingProblem,
141
146
  ColumnTrackSpec,
142
147
  DomainBand,
@@ -154,14 +159,11 @@ import type {
154
159
  ResolvedClade,
155
160
  ResolvedEncoding,
156
161
  ResolvedFeaturePanel,
162
+ ResolvedStripPanel,
157
163
  ResolvedHighlight,
158
164
  ResolvedSelection,
159
- TreeOrder,
160
165
  TreeRoot,
161
166
  ResolvedRowPanel,
162
- ResolvedStripPanel,
163
- RowFeaturesSpec,
164
- RowPanelSpan,
165
167
  RowDataInput,
166
168
  RowPanelSpec,
167
169
  RowResidue,
@@ -170,17 +172,11 @@ import type {
170
172
  UnshareableData,
171
173
  Viewport,
172
174
  } from './types.ts'
175
+ import type { ZoomSnapshot } from './zoomSnapshot.ts'
173
176
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
174
177
  import type { Instance } from '@jbrowse/mobx-state-tree'
175
178
  import type { IComputedValue } from 'mobx'
176
179
 
177
- function parseTreeText(text: string) {
178
- if (text.startsWith('BioTreeContainer')) {
179
- return flatToTree(parseAsn1(text))
180
- }
181
- return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
182
- }
183
-
184
180
  // The height each kind draws at before the user drags a divider. A text track
185
181
  // is absent: it is one alignment row tall and follows rowHeight, so the zoom
186
182
  // controls already size it.
@@ -209,262 +205,6 @@ const ownHeightKey = (id: string) => `own:${id}`
209
205
  const HELIX_ARC = '#4e79a7'
210
206
  const PSEUDOKNOT_ARC = '#e15759'
211
207
 
212
- // the fill of a span under a scale that gives its value no color, such as a
213
- // gene a `map` over gene names leaves out
214
- const UNSCALED_FEATURE = '#d9d9d9'
215
-
216
- // the fraction of a span a following one may cover before it takes a lane of
217
- // its own in a `features` panel
218
- const laneOverlap = 0.1
219
-
220
- // a row panel's own width, or a default: the row height, which makes a strip
221
- // cell square, and a gene neighborhood's width for a features panel. The row
222
- // height is read only when used, so a panel with a width of its own does not
223
- // recompute on a vertical zoom
224
- function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
225
- return (
226
- panel.width ??
227
- (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
228
- )
229
- }
230
-
231
- // one row of the row table as the channels read it. A number or a boolean
232
- // reads as its string, the way a GFF value does, and null or an object drops
233
- function stringFields(row: unknown) {
234
- if (typeof row !== 'object' || row === null) {
235
- return undefined
236
- }
237
- const fields: Record<string, string> = {}
238
- for (const [field, value] of Object.entries(row)) {
239
- if (typeof value === 'string') {
240
- fields[field] = value
241
- } else if (typeof value === 'number' || typeof value === 'boolean') {
242
- fields[field] = String(value)
243
- }
244
- }
245
- return fields
246
- }
247
-
248
- // the color each row takes from a field of the row table under a scale
249
- function colorByRow(
250
- rowData: Record<string, Record<string, string> | undefined>,
251
- field: string,
252
- colorOf: (value: string) => string | undefined,
253
- ) {
254
- const colors = new Map<string, string>()
255
- for (const [name, row] of Object.entries(rowData)) {
256
- const value = row?.[field]
257
- const color = value === undefined ? undefined : colorOf(value)
258
- if (color) {
259
- colors.set(name, color)
260
- }
261
- }
262
- return colors
263
- }
264
-
265
- type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
266
- type FeaturePanelScale = Omit<
267
- ResolvedFeaturePanel,
268
- 'id' | 'width' | 'offsetX' | 'spans'
269
- >
270
-
271
- // the fill and outline of every feature in a list: its own GFF `color=` first,
272
- // then the scale an encoding resolves over one of its fields, then the
273
- // accession palette
274
- function featureColorMap(
275
- annotations: Annotation[],
276
- encoding:
277
- | { field: string; colorOf: (value: string) => string | undefined }
278
- | undefined,
279
- fillPalette: Record<string, string>,
280
- ) {
281
- const strokes = new Map<string, string>()
282
- const strokeOf = (fill: string) => {
283
- const hit = strokes.get(fill)
284
- if (hit !== undefined) {
285
- return hit
286
- }
287
- const stroke = outlineColor(fill)
288
- strokes.set(fill, stroke)
289
- return stroke
290
- }
291
- return new Map(
292
- annotations.map(annotation => {
293
- const value = encoding
294
- ? featureField(annotation, encoding.field)
295
- : undefined
296
- const fill =
297
- annotation.color ??
298
- (value === undefined ? undefined : encoding!.colorOf(value)) ??
299
- (encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
300
- return [annotation, { fill, stroke: strokeOf(fill) }]
301
- }),
302
- )
303
- }
304
-
305
- // the text a label channel draws inside each span, for the features carrying
306
- // the field it names
307
- function featureLabelMap(annotations: Annotation[], field: string) {
308
- const labels = new Map<Annotation, string>()
309
- for (const annotation of annotations) {
310
- const value = featureField(annotation, field)
311
- if (value !== undefined) {
312
- labels.set(annotation, value)
313
- }
314
- }
315
- return labels
316
- }
317
-
318
- /** what `packDomainLanes` needs to lay a row out, before it has its lane */
319
- type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
320
- startCol: number
321
- endCol: number
322
- }
323
-
324
- /**
325
- * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
326
- * forward features stack above the line and the reverse below it, each strand
327
- * packing on its own. The deepest row on each side sets the grid every row lays
328
- * out on, so the line between the strands sits at one height down the panel and
329
- * a reader can scan it. A feature with no strand piles with the forward ones.
330
- */
331
- function strandpileLanes(
332
- rows: [string, Unlaned[]][],
333
- ): Map<string, RowPanelSpan[]> {
334
- const piled = rows.map(([name, bands]) => {
335
- const reverse = packDomainLanes(
336
- bands.filter(b => b.annotation.strand === -1),
337
- )
338
- const forward = packDomainLanes(
339
- bands.filter(b => b.annotation.strand !== -1),
340
- )
341
- return { name, forward, reverse }
342
- })
343
- const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
344
- const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
345
- const laneCount = up + down
346
- return new Map(
347
- piled.map(({ name, forward, reverse }) => [
348
- name,
349
- [
350
- // forward level 0 is the lane just above the line, stacking upward
351
- ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
352
- ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
353
- ],
354
- ]),
355
- )
356
- }
357
-
358
- /** one row's spans laid out by the panel's `position` */
359
- function panelLanes(
360
- panel: RowFeaturesSpec,
361
- rows: [string, Unlaned[]][],
362
- ): Map<string, RowPanelSpan[]> {
363
- return panel.position === 'strandpile'
364
- ? strandpileLanes(rows)
365
- : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
366
- }
367
-
368
- /**
369
- * The spans a `features` panel draws, keyed by row name and measured in the
370
- * panel's own pixels. `column` takes the bands the overlay draws, at the
371
- * alignment's column width. `position` packs each row's features in its own
372
- * residue positions, shifted by an `align` transform, and maps the extent they
373
- * cover across every row onto the panel width, so a row with no alignment
374
- * still has an x.
375
- */
376
- function featurePanelSpans({
377
- panel,
378
- width,
379
- colWidth,
380
- domainBands,
381
- annotationsByRow,
382
- shifts,
383
- }: {
384
- panel: RowFeaturesSpec
385
- width: number
386
- colWidth: number
387
- domainBands: Map<string, DomainBand[]>
388
- annotationsByRow: Record<string, Annotation[]>
389
- shifts: Map<string, number> | undefined
390
- }): Map<string, RowPanelSpan[]> {
391
- if (panel.x === 'column') {
392
- return panelLanes(
393
- panel,
394
- [...domainBands].map(([name, bands]) => [
395
- name,
396
- bands.map(band => ({
397
- ...band,
398
- xStart: band.startCol * colWidth,
399
- xEnd: band.endCol * colWidth,
400
- })),
401
- ]),
402
- )
403
- }
404
- const shifted = Object.entries(annotationsByRow).map(
405
- ([name, annotations]) => {
406
- const shift = shifts?.get(name) ?? 0
407
- return [
408
- name,
409
- annotations.map(annotation => ({
410
- annotation,
411
- start: annotation.start - 1 + shift,
412
- end: annotation.end + shift,
413
- })),
414
- ] as const
415
- },
416
- )
417
- let min = Infinity
418
- let max = -Infinity
419
- for (const [, features] of shifted) {
420
- for (const { start, end } of features) {
421
- min = Math.min(min, start)
422
- max = Math.max(max, end)
423
- }
424
- }
425
- // a gene arrow ends where its feature does, so the extent maps onto the whole
426
- // panel less the pixel the rightmost stroke needs
427
- const drawable = Math.max(1, width - 1)
428
- const scale = max > min ? drawable / (max - min) : 0
429
- // a lane opens where a span covers more than a tenth of the one before it,
430
- // which keeps the genes of an operon on one lane: adjacent genes commonly
431
- // share a few bases, and a stop codon overlapping the next start reads as a
432
- // second lane over the whole row
433
- return panelLanes(
434
- panel,
435
- shifted.map(([name, features]) => [
436
- name,
437
- features.map(({ annotation, start, end }) => ({
438
- annotation,
439
- xStart: (start - min) * scale,
440
- xEnd: (end - min) * scale,
441
- startCol: start,
442
- endCol: end - (end - start) * laneOverlap,
443
- })),
444
- ]),
445
- )
446
- }
447
-
448
- // a scale over the values a field takes across every feature loaded, so hiding
449
- // a type in the filter dialog leaves the rest in their colors, and a legend of
450
- // the values the features drawn carry
451
- function resolveFeatureScale(
452
- field: string,
453
- scale: ScaleSpec | undefined,
454
- annotations: Annotation[],
455
- drawn: Annotation[],
456
- ) {
457
- const valuesOf = (list: Annotation[]) =>
458
- list.map(a => featureField(a, field)).filter(notEmpty)
459
- const resolved = resolveScale(scale, valuesOf(annotations))
460
- const shown = new Set(valuesOf(drawn))
461
- return {
462
- field,
463
- ...resolved,
464
- legend: resolved.legend.filter(entry => shown.has(entry.id)),
465
- }
466
- }
467
-
468
208
  const columnTrackBytes = new WeakMap<ColumnTrackSpec, number>()
469
209
 
470
210
  function columnTrackSize(track: ColumnTrackSpec) {
@@ -524,6 +264,7 @@ export const preservedOnReset = new Set([
524
264
  'drawNodeBubbles',
525
265
  'drawNodeLabels',
526
266
  'showTreeOverview',
267
+ 'showTreeScaleBar',
527
268
  'overviewHeight',
528
269
  'turnedOffTracks',
529
270
  'trackHeights',
@@ -531,266 +272,23 @@ export const preservedOnReset = new Set([
531
272
  'allowedGappyness',
532
273
  'subFeatureRows',
533
274
  'showDomainLegend',
275
+ 'domainFill',
276
+ 'embeddedMode',
534
277
  ])
535
278
 
536
279
  // `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
537
- // track writes nothing into the snapshot. A file-supplied track passes its own
538
- // `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
539
- // lines, an Rfam family one per row.
280
+ // track writes nothing into the snapshot
540
281
  function trackIsOff(
541
282
  turnedOffTracks: { get: (id: string) => boolean | undefined },
542
283
  id: string,
543
- defaultOff?: boolean,
544
284
  ) {
545
- return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
285
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
546
286
  }
547
287
 
548
- // the marks that set tree state from a node rather than drawing
549
- const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
550
-
551
288
  // shared empty results, so observers don't see a fresh [] as a change
552
289
  const noDomains: Annotation[] = []
553
290
  const noClades: ResolvedClade[] = []
554
291
 
555
- /**
556
- * Each tip of the full tree in the order the tree draws it, with the on-screen
557
- * row that shows it: its own, or the row of the collapsed node standing for it
558
- */
559
- function tipRows(root: HierarchyNode<NodeWithIds>) {
560
- const rows = new Map<string, { order: number; row: number }>()
561
- let row = 0
562
- const pushReversed = (
563
- stack: HierarchyNode<NodeWithIds>[],
564
- nodes: HierarchyNode<NodeWithIds>[],
565
- ) => {
566
- for (let i = nodes.length - 1; i >= 0; i--) {
567
- stack.push(nodes[i]!)
568
- }
569
- }
570
- const stack = [root]
571
- while (stack.length > 0) {
572
- const node = stack.pop()!
573
- if (node.children?.length) {
574
- pushReversed(stack, node.children)
575
- } else {
576
- const hidden = [node]
577
- while (hidden.length > 0) {
578
- const tip = hidden.pop()!
579
- const kids = tip.children ?? tip._children
580
- if (kids?.length) {
581
- pushReversed(hidden, kids)
582
- } else {
583
- rows.set(tip.data.name, { order: rows.size, row })
584
- }
585
- }
586
- row++
587
- }
588
- }
589
- return rows
590
- }
591
-
592
- /**
593
- * The rows a clade covers and the node an `mrca` names, or undefined when the
594
- * clade does not resolve: a tip name the tree does not have or has twice, or a
595
- * leaf count `tips` disagrees with. A `range` counts its tips in the order
596
- * `tips` gives them, so a collapsed clade inside it counts every leaf it
597
- * stands for. `index` is the memoized leaf pass over the tree.
598
- */
599
- function cladeRows(
600
- clade: Clade,
601
- root: HierarchyNode<NodeWithIds>,
602
- index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
603
- tips: Map<string, { order: number; row: number }>,
604
- ): { rows: [number, number]; nodeId?: string } | undefined {
605
- if (clade.range) {
606
- const [a, b] = clade.range.map(name =>
607
- index.get(name) ? tips.get(name) : undefined,
608
- )
609
- if (a === undefined || b === undefined) {
610
- return undefined
611
- }
612
- const [first, last] = a.order <= b.order ? [a, b] : [b, a]
613
- return last.order - first.order + 1 === clade.tips
614
- ? { rows: [first.row, last.row] }
615
- : undefined
616
- }
617
- const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
618
- if (!node) {
619
- return undefined
620
- }
621
- const nodeTips = leaves(node)
622
- if (nodeTips.length !== clade.tips) {
623
- return undefined
624
- }
625
- let first = Infinity
626
- let last = -Infinity
627
- for (const tip of nodeTips) {
628
- const row = tips.get(tip.data.name)?.row
629
- if (row !== undefined) {
630
- first = Math.min(first, row)
631
- last = Math.max(last, row)
632
- }
633
- }
634
- return first <= last
635
- ? { rows: [first, last], nodeId: node.data.id }
636
- : undefined
637
- }
638
-
639
- /**
640
- * Every clade that resolves, with its fill color settled, against the rows of
641
- * `displayed`, the tree as drawn. The tree panel resolves against the
642
- * displayed rows and the overview against its own, so both take the tree they
643
- * draw. A `range` record names no node, so the seeding marks, which need one,
644
- * drop it.
645
- */
646
- function resolveClades(
647
- clades: Clade[],
648
- root: HierarchyNode<NodeWithIds>,
649
- displayed: HierarchyNode<NodeWithIds>,
650
- ): ResolvedClade[] {
651
- const index = leafIndex(root)
652
- const tips = tipRows(displayed)
653
- return clades.flatMap(clade => {
654
- const resolved = cladeRows(clade, root, index, tips)
655
- const seeding = seedingMarks.has(clade.mark)
656
- return resolved && !(seeding && resolved.nodeId === undefined)
657
- ? [
658
- {
659
- ...resolved,
660
- mark: clade.mark,
661
- color: withAlpha(
662
- clade.color ?? cladeHighlightColor,
663
- cladeHighlightAlpha,
664
- ),
665
- markColor: clade.color,
666
- label: clade.label,
667
- },
668
- ]
669
- : []
670
- })
671
- }
672
-
673
- const childOrders: Record<
674
- TreeOrder,
675
- ((a: HierarchyNode, b: HierarchyNode) => number) | undefined
676
- > = {
677
- input: undefined,
678
- branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
679
- ladderize: (a, b) => a.value! - b.value!,
680
- ladderizeReverse: (a, b) => b.value! - a.value!,
681
- }
682
-
683
- /**
684
- * The tree with the display transforms applied: leaf counts summed, children
685
- * put in `order` and each `rotated` node's reversed, `showOnly` taken as the
686
- * new root, and each `collapsed` clade folded. The overview builds the same
687
- * tree without the focus, so it shows the focused subtree inside the whole.
688
- */
689
- function buildTreeRoot(
690
- tree: NodeWithIds,
691
- {
692
- collapsed,
693
- rotated,
694
- order,
695
- showOnly,
696
- }: {
697
- collapsed: readonly string[]
698
- rotated: readonly string[]
699
- order: TreeOrder
700
- showOnly?: string
701
- },
702
- ) {
703
- let hier = hierarchy(tree, d => d.children)
704
- hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
705
- const compare = childOrders[order]
706
- if (compare) {
707
- sort(hier, compare)
708
- }
709
- if (rotated.length > 0) {
710
- const flip = new Set(rotated)
711
- forEachDescendant(hier, n => {
712
- if (flip.has(n.data.id)) {
713
- n.children?.reverse()
714
- }
715
- })
716
- }
717
-
718
- if (showOnly) {
719
- const res = find(hier, n => n.data.id === showOnly)
720
- if (res) {
721
- hier = res
722
- }
723
- }
724
-
725
- for (const collapsedId of collapsed) {
726
- const node = find(hier, n => n.data.id === collapsedId)
727
- if (node) {
728
- if (node.children) {
729
- collapse(node)
730
- } else if (node.parent?.children) {
731
- node.parent.children = node.parent.children.filter(
732
- c => c.data.id !== collapsedId,
733
- )
734
- }
735
- }
736
- }
737
-
738
- return hier
739
- }
740
-
741
- /**
742
- * The node whose branch an outgroup roots the tree on: the outgroup's most
743
- * recent common ancestor, or the rest of the tips' where that ancestor is the
744
- * root, since the outgroup then straddles the root and the branch above the
745
- * other side is the same branch of the unrooted tree.
746
- */
747
- function outgroupNode(root: HierarchyNode, outgroup: string[]) {
748
- const index = leafIndex(root)
749
- const node = mrca(root, outgroup, index)
750
- if (node !== root) {
751
- return node
752
- }
753
- const out = new Set(outgroup)
754
- const rest = leaves(root)
755
- .map(n => n.data.name)
756
- .filter(name => !out.has(name))
757
- const other = mrca(root, rest, index)
758
- return other === root ? undefined : other
759
- }
760
-
761
- /**
762
- * `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
763
- * as written become names on unnamed nodes, and a rerooting would carry them
764
- * along as labels, so they come off first.
765
- */
766
- function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
767
- const plain = new Map<NodeWithIds, TreeNode>()
768
- for (const node of preorder(tree).reverse()) {
769
- plain.set(node, {
770
- name: node.name === node.id ? undefined : node.name,
771
- length: node.length,
772
- children: node.children.map(c => plain.get(c)!),
773
- })
774
- }
775
- const input = plain.get(tree)!
776
- if (treeRoot === 'midpoint') {
777
- return generateNodeIds(midpointRoot(input))
778
- }
779
- const target = outgroupNode(
780
- hierarchy(tree, d => d.children),
781
- treeRoot.outgroup,
782
- )
783
- return target
784
- ? generateNodeIds(rerootTree(input, plain.get(target.data)!))
785
- : tree
786
- }
787
-
788
- // the inclusive tip indices a node covers, from its xMin/xMax in a layout one
789
- // row per tip
790
- function tipRange(node: HierarchyNode): [number, number] {
791
- return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
792
- }
793
-
794
292
  // the channels reading the feature table; every other channel reads rowData
795
293
  const featureChannels = new Set<EncodingChannel>([
796
294
  'featureFill',
@@ -821,6 +319,31 @@ function sameLength(segment: ResidueSegment) {
821
319
  )
822
320
  }
823
321
 
322
+ // A zoom that passes the default cell size stops on it, and one that starts on
323
+ // it holds there until the wheel has gathered `zoomDetent` worth of scale, so
324
+ // a zoom in or out finds its way back to the default
325
+ const zoomDetent = Math.log(1.3)
326
+
327
+ function detentZoom(
328
+ value: number,
329
+ scale: number,
330
+ home: number,
331
+ held: number,
332
+ min: number,
333
+ canHold: boolean,
334
+ ) {
335
+ if (value === home && canHold) {
336
+ const total = held + Math.log(scale)
337
+ return Math.abs(total) < zoomDetent
338
+ ? { value, held: total }
339
+ : { value: clamp(home * Math.exp(total), min, maxCellSize), held: 0 }
340
+ }
341
+ const next = clamp(value * scale, min, maxCellSize)
342
+ return (value - home) * (next - home) < 0
343
+ ? { value: home, held: 0 }
344
+ : { value: next, held: 0 }
345
+ }
346
+
824
347
  // fit() divides the viewport by the row or column count and multiplies back, so
825
348
  // an exact fit can land a fraction of a pixel over. A plain `>` would then show
826
349
  // a scrollbar that shrinks the viewport and leaves a gap.
@@ -855,19 +378,11 @@ function sharedValue(values: (string | undefined)[]) {
855
378
  : undefined
856
379
  }
857
380
 
858
- // preorder, so reversing the list puts every node after its descendants
859
- function preorder(tree: NodeWithIds) {
860
- const order: NodeWithIds[] = []
861
- const stack = [tree]
862
- while (stack.length > 0) {
863
- const node = stack.pop()!
864
- order.push(node)
865
- for (const child of node.children) {
866
- stack.push(child)
867
- }
868
- }
869
- return order
870
- }
381
+ type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
382
+ type FeaturePanelScale = Omit<
383
+ ResolvedFeaturePanel,
384
+ 'id' | 'width' | 'offsetX' | 'spans'
385
+ >
871
386
 
872
387
  /**
873
388
  * #stateModel MsaView
@@ -913,6 +428,13 @@ function stateModelFactory({
913
428
  * #property
914
429
  */
915
430
  showDomains: stripDefault(types.boolean, defaultShowDomains),
431
+ /**
432
+ * #property
433
+ * whether shown domains fill the cells they cover. Off, the residue
434
+ * scheme keeps the cells and each domain draws as a bar under its
435
+ * row. See `cellFill`.
436
+ */
437
+ domainFill: stripDefault(types.boolean, true),
916
438
  /**
917
439
  * #property
918
440
  * whether the domain legend is expanded. The legend floats over the
@@ -944,6 +466,13 @@ function stateModelFactory({
944
466
  */
945
467
  drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
946
468
 
469
+ /**
470
+ * #property
471
+ * draws the toolbar's menus and Settings as icons without text, for a
472
+ * host with little room, such as a JBrowse view
473
+ */
474
+ embeddedMode: stripDefault(types.boolean, false),
475
+
947
476
  /**
948
477
  * #property
949
478
  * zoom in/out on plain mouse-wheel without holding ctrl
@@ -1096,10 +625,10 @@ function stateModelFactory({
1096
625
 
1097
626
  /**
1098
627
  * #property
1099
- * the user's explicit hide choices per annotation accession, keyed by
1100
- * accession with the value meaning "off", like `turnedOffTracks`. An
1101
- * untouched accession is absent and drawn, so the shared URL grows
1102
- * only with the user's filters
628
+ * the user's departures from each annotation accession's default,
629
+ * keyed by accession with the value meaning "off", like
630
+ * `turnedOffTracks`. An untouched accession is absent and takes its
631
+ * default, so the shared URL grows only with the user's filters
1103
632
  */
1104
633
  turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
1105
634
  /**
@@ -1180,6 +709,18 @@ function stateModelFactory({
1180
709
  * #volatile
1181
710
  */
1182
711
  headerHeight: 0,
712
+ /**
713
+ * #volatile
714
+ * whether the settings panel docks beside the alignment, taking
715
+ * `settingsPanelWidth` out of the width the view lays out in
716
+ */
717
+ settingsOpen: false,
718
+ /**
719
+ * #volatile
720
+ * the log scale a wheel zoom has gathered against the detent at the
721
+ * default column width and row height, per axis
722
+ */
723
+ zoomHeld: { col: 0, row: 0 },
1183
724
  /**
1184
725
  * #volatile
1185
726
  * leaves the toolbar out, for a host drawing its own controls. Kept out
@@ -1203,6 +744,13 @@ function stateModelFactory({
1203
744
  highResScaleFactor:
1204
745
  typeof window === 'undefined' ? 1 : window.devicePixelRatio,
1205
746
 
747
+ /**
748
+ * #volatile
749
+ * the cell size and block grid the alignment last drew at, held from a
750
+ * wheel-zoom frame until the gesture settles. MSACanvas stretches those
751
+ * blocks to the current cell size with one CSS transform
752
+ */
753
+ zoomSnapshot: undefined as ZoomSnapshot | undefined,
1206
754
  /**
1207
755
  * #volatile
1208
756
  */
@@ -1365,6 +913,12 @@ function stateModelFactory({
1365
913
  setWidth(arg: number) {
1366
914
  self.volatileWidth = arg
1367
915
  },
916
+ /**
917
+ * #action
918
+ */
919
+ setSettingsOpen(arg: boolean) {
920
+ self.settingsOpen = arg
921
+ },
1368
922
  /**
1369
923
  * #action
1370
924
  * update the canvas scale factor when the device pixel ratio changes
@@ -1508,6 +1062,23 @@ function stateModelFactory({
1508
1062
  setShowDomains(arg: boolean) {
1509
1063
  self.showDomains = arg
1510
1064
  },
1065
+ /**
1066
+ * #action
1067
+ */
1068
+ setDomainFill(arg: boolean) {
1069
+ self.domainFill = arg
1070
+ },
1071
+ /**
1072
+ * #action
1073
+ * hand the cell fill to the residue scheme, the domains, or neither,
1074
+ * which colors the letters instead
1075
+ */
1076
+ setCellFill(mode: 'residue' | 'domain' | 'none') {
1077
+ self.setBgColor(mode !== 'none')
1078
+ if (mode !== 'none') {
1079
+ self.domainFill = mode === 'domain'
1080
+ }
1081
+ },
1511
1082
  /**
1512
1083
  * #action
1513
1084
  * expand or collapse the domain legend that floats over the alignment
@@ -1536,6 +1107,7 @@ function stateModelFactory({
1536
1107
  */
1537
1108
  setRowHeight(n: number) {
1538
1109
  self.rowHeight = n
1110
+ self.zoomHeld = { ...self.zoomHeld, row: 0 }
1539
1111
  },
1540
1112
 
1541
1113
  /**
@@ -1544,6 +1116,7 @@ function stateModelFactory({
1544
1116
  */
1545
1117
  setColWidth(n: number) {
1546
1118
  self.colWidth = n
1119
+ self.zoomHeld = { ...self.zoomHeld, col: 0 }
1547
1120
  },
1548
1121
 
1549
1122
  /**
@@ -1715,22 +1288,7 @@ function stateModelFactory({
1715
1288
  self.allowedGappyness < 100)
1716
1289
  )
1717
1290
  },
1718
- /**
1719
- * #getter
1720
- * the annotations the overlay, the legend and the encodings draw: the
1721
- * GFF's, then the `features` layer's
1722
- */
1723
- get allAnnotations(): Annotation[] {
1724
- return self.features.length > 0
1725
- ? [...self.annotations, ...featuresToAnnotations(self.features)]
1726
- : self.annotations
1727
- },
1728
- /**
1729
- * #getter
1730
- */
1731
- get actuallyShowDomains() {
1732
- return self.showDomains && this.allAnnotations.length > 0
1733
- },
1291
+
1734
1292
  /**
1735
1293
  * #getter
1736
1294
  * whether the host restores the loaded documents outside the snapshot.
@@ -1755,7 +1313,7 @@ function stateModelFactory({
1755
1313
  if (self.volatileWidth === undefined) {
1756
1314
  throw new Error('not initialized')
1757
1315
  }
1758
- return self.volatileWidth
1316
+ return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0)
1759
1317
  },
1760
1318
  }))
1761
1319
  .views(self => ({
@@ -1805,7 +1363,7 @@ function stateModelFactory({
1805
1363
  return !!this.tree.noTree
1806
1364
  },
1807
1365
  get noDomains() {
1808
- return self.allAnnotations.length === 0
1366
+ return this.allAnnotations.length === 0
1809
1367
  },
1810
1368
 
1811
1369
  /**
@@ -1914,6 +1472,27 @@ function stateModelFactory({
1914
1472
  ? parseMSA(text, self.currentAlignment, self.msaFormat)
1915
1473
  : null
1916
1474
  },
1475
+ /**
1476
+ * #getter
1477
+ * the annotations the overlay, the legend and the encodings draw: the
1478
+ * GFF's, the `features` layer's, then the file's own per-row lines
1479
+ */
1480
+ get allAnnotations(): Annotation[] {
1481
+ const inline = this.MSA?.annotations ?? []
1482
+ return self.features.length > 0 || inline.length > 0
1483
+ ? [
1484
+ ...self.annotations,
1485
+ ...featuresToAnnotations(self.features),
1486
+ ...inline,
1487
+ ]
1488
+ : self.annotations
1489
+ },
1490
+ /**
1491
+ * #getter
1492
+ */
1493
+ get actuallyShowDomains() {
1494
+ return self.showDomains && this.allAnnotations.length > 0
1495
+ },
1917
1496
  /**
1918
1497
  * #getter
1919
1498
  */
@@ -2507,6 +2086,22 @@ function stateModelFactory({
2507
2086
  )
2508
2087
  },
2509
2088
 
2089
+ /**
2090
+ * #getter
2091
+ * Pre-computed similarity shades per column, for blosum62_dynamic.
2092
+ * Returns a map of letter -> color for each column. Amino acids shade by
2093
+ * BLOSUM62 similarity to the rest of the column, and an alignment of
2094
+ * nucleotides by identity, since the matrix names amino acids.
2095
+ */
2096
+ get colSimilarity() {
2097
+ const { colStats, sequenceType } = this
2098
+ const columnColors =
2099
+ sequenceType === 'amino' ? blosum62ColumnColors : identityColumnColors
2100
+ return Array.from({ length: colStats.numColumns }, (_, col) =>
2101
+ columnColors(colStats, col),
2102
+ )
2103
+ },
2104
+
2510
2105
  /**
2511
2106
  * #getter
2512
2107
  * Conservation score per column using Shannon entropy (biojs-msa style).
@@ -2903,6 +2498,30 @@ function stateModelFactory({
2903
2498
  }))
2904
2499
  },
2905
2500
 
2501
+ /**
2502
+ * #getter
2503
+ * the reference row (`relativeTo`) repeated above the alignment, so it
2504
+ * stays in view while the rows compared against it scroll
2505
+ */
2506
+ get referenceTrackModels(): BasicTrack[] {
2507
+ const { relativeTo } = self
2508
+ const data =
2509
+ relativeTo === undefined ? undefined : self.columns.get(relativeTo)
2510
+ return relativeTo === undefined || data === undefined
2511
+ ? []
2512
+ : [
2513
+ {
2514
+ model: {
2515
+ id: 'reference-row',
2516
+ name: relativeTo,
2517
+ kind: 'reference' as const,
2518
+ data,
2519
+ height: self.rowHeight,
2520
+ },
2521
+ },
2522
+ ]
2523
+ },
2524
+
2906
2525
  get tracks(): BasicTrack[] {
2907
2526
  return [
2908
2527
  ...this.adapterTrackModels,
@@ -2911,6 +2530,7 @@ function stateModelFactory({
2911
2530
  // every computed track reads the alignment's columns, and a tree, a
2912
2531
  // GFF and a features panel draw a figure with none
2913
2532
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
2533
+ ...this.referenceTrackModels,
2914
2534
  ]
2915
2535
  },
2916
2536
 
@@ -2919,8 +2539,7 @@ function stateModelFactory({
2919
2539
  */
2920
2540
  get turnedOnTracks() {
2921
2541
  return this.tracks.filter(
2922
- f =>
2923
- !trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff),
2542
+ f => !trackIsOff(self.turnedOffTracks, f.model.id),
2924
2543
  )
2925
2544
  },
2926
2545
 
@@ -3250,7 +2869,11 @@ function stateModelFactory({
3250
2869
  if (mapping.structure.id !== structureId) {
3251
2870
  continue
3252
2871
  }
3253
- if (asymId !== undefined && mapping.structure.asymId !== asymId) {
2872
+ if (
2873
+ asymId !== undefined &&
2874
+ mapping.structure.asymId !== undefined &&
2875
+ mapping.structure.asymId !== asymId
2876
+ ) {
3254
2877
  continue
3255
2878
  }
3256
2879
  const segment = mapping.segments.find(
@@ -3270,10 +2893,14 @@ function stateModelFactory({
3270
2893
  .views(self => ({
3271
2894
  /**
3272
2895
  * #getter
3273
- * the branch-length scale bar over the tree, undefined in cladogram mode
3274
- * or when the tree area is too narrow for one
2896
+ * the branch-length scale bar over the tree, undefined while
2897
+ * `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
2898
+ * too narrow for one
3275
2899
  */
3276
2900
  get treeScaleBar() {
2901
+ if (!self.showTreeScaleBar) {
2902
+ return undefined
2903
+ }
3277
2904
  return scaleBarLength(
3278
2905
  self.pxPerBranchLength,
3279
2906
  self.effectiveTreeAreaWidth - self.marginLeft * 2,
@@ -3329,11 +2956,43 @@ function stateModelFactory({
3329
2956
  }
3330
2957
  return types
3331
2958
  },
3332
- get filteredAnnotations() {
3333
- return self.allAnnotations.filter(
3334
- r => !self.turnedOffFeatures.get(r.accession),
2959
+ /**
2960
+ * accessions hidden until the user turns them on, such as a Stockholm
2961
+ * file's #=GR lines
2962
+ */
2963
+ get defaultOffAccessions() {
2964
+ return new Set(
2965
+ self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession),
2966
+ )
2967
+ },
2968
+ /**
2969
+ * #method
2970
+ */
2971
+ featureOff(accession: string) {
2972
+ return (
2973
+ self.turnedOffFeatures.get(accession) ??
2974
+ this.defaultOffAccessions.has(accession)
3335
2975
  )
3336
2976
  },
2977
+ get filteredAnnotations() {
2978
+ return self.allAnnotations.filter(r => !this.featureOff(r.accession))
2979
+ },
2980
+ /**
2981
+ * #getter
2982
+ * what colors the alignment's cells: the residue scheme, the domains
2983
+ * covering them, or nothing, which leaves the scheme to the letters.
2984
+ * Domains own the fill only while some are drawn, so a file whose
2985
+ * annotations all start hidden keeps its residue colors
2986
+ */
2987
+ get cellFill(): 'residue' | 'domain' | 'none' {
2988
+ return !self.bgColor
2989
+ ? 'none'
2990
+ : self.actuallyShowDomains &&
2991
+ self.domainFill &&
2992
+ this.filteredAnnotations.length > 0
2993
+ ? 'domain'
2994
+ : 'residue'
2995
+ },
3337
2996
  get annotationsByRow() {
3338
2997
  const { rowNamesSet } = self
3339
2998
  return groupBy(this.filteredAnnotations, r =>
@@ -3412,6 +3071,12 @@ function stateModelFactory({
3412
3071
  self.blocksX.map(bx => [bx, by] as const),
3413
3072
  )
3414
3073
  },
3074
+ /**
3075
+ * #getter
3076
+ */
3077
+ get wheelZooming() {
3078
+ return self.zoomSnapshot !== undefined
3079
+ },
3415
3080
 
3416
3081
  /**
3417
3082
  * #getter
@@ -3637,19 +3302,35 @@ function stateModelFactory({
3637
3302
  transaction(() => {
3638
3303
  const colInView = (-self.scrollX + offsetX) / self.colWidth
3639
3304
  const rowInView = (-self.scrollY + offsetY) / self.rowHeight
3305
+ // zooming both axes, an axis holds at the default only while the
3306
+ // other sits there too, or one would hold while the other grows
3307
+ const canHold =
3308
+ axis !== 'both' ||
3309
+ (self.colWidth === defaultColWidth &&
3310
+ self.rowHeight === defaultRowHeight)
3640
3311
  if (axis !== 'vertical') {
3641
- self.colWidth = clamp(
3642
- self.colWidth * scaleFactor,
3312
+ const { value, held } = detentZoom(
3313
+ self.colWidth,
3314
+ scaleFactor,
3315
+ defaultColWidth,
3316
+ self.zoomHeld.col,
3643
3317
  minColWidth,
3644
- maxCellSize,
3318
+ canHold,
3645
3319
  )
3320
+ self.colWidth = value
3321
+ self.zoomHeld = { ...self.zoomHeld, col: held }
3646
3322
  }
3647
3323
  if (axis !== 'horizontal') {
3648
- self.rowHeight = clamp(
3649
- self.rowHeight * scaleFactor,
3324
+ const { value, held } = detentZoom(
3325
+ self.rowHeight,
3326
+ scaleFactor,
3327
+ defaultRowHeight,
3328
+ self.zoomHeld.row,
3650
3329
  minRowHeight,
3651
- maxCellSize,
3330
+ canHold,
3652
3331
  )
3332
+ self.rowHeight = value
3333
+ self.zoomHeld = { ...self.zoomHeld, row: held }
3653
3334
  }
3654
3335
  self.scrollX = clamp(
3655
3336
  offsetX - colInView * self.colWidth,
@@ -3670,6 +3351,25 @@ function stateModelFactory({
3670
3351
  )
3671
3352
  })
3672
3353
  },
3354
+ /**
3355
+ * #action
3356
+ * zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
3357
+ * it drew at the start of the gesture, scaled, and redraws them only once
3358
+ * they stop covering the viewport; endWheelZoom redraws them sharp
3359
+ */
3360
+ wheelZoomToPos(scaleFactor: number, offsetX: number, offsetY: number) {
3361
+ self.zoomSnapshot ??= takeZoomSnapshot(self, false)
3362
+ this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis)
3363
+ if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
3364
+ self.zoomSnapshot = takeZoomSnapshot(self, true)
3365
+ }
3366
+ },
3367
+ /**
3368
+ * #action
3369
+ */
3370
+ endWheelZoom() {
3371
+ self.zoomSnapshot = undefined
3372
+ },
3673
3373
  /**
3674
3374
  * #action
3675
3375
  */
@@ -3754,11 +3454,22 @@ function stateModelFactory({
3754
3454
  * #action
3755
3455
  */
3756
3456
  toggleTrack(id: string) {
3757
- const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff
3758
- self.turnedOffTracks.set(
3759
- id,
3760
- !trackIsOff(self.turnedOffTracks, id, defaultOff),
3761
- )
3457
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
3458
+ },
3459
+ /**
3460
+ * #action
3461
+ * show or hide several tracks at once, as the track selector's Show all
3462
+ * and Hide all do. A choice matching the track's default is dropped, so
3463
+ * it stays out of the shared URL
3464
+ */
3465
+ setTracksShown(ids: string[], shown: boolean) {
3466
+ for (const id of ids) {
3467
+ if (defaultOffTracks.has(id) === !shown) {
3468
+ self.turnedOffTracks.delete(id)
3469
+ } else {
3470
+ self.turnedOffTracks.set(id, !shown)
3471
+ }
3472
+ }
3762
3473
  },
3763
3474
  /**
3764
3475
  * #action
@@ -3923,7 +3634,7 @@ function stateModelFactory({
3923
3634
  */
3924
3635
  get visibleDomainTypes() {
3925
3636
  return this.categoricalDomainTypes
3926
- .filter(d => !self.turnedOffFeatures.get(d.accession))
3637
+ .filter(d => !self.featureOff(d.accession))
3927
3638
  .toSorted((a, b) => a.start - b.start)
3928
3639
  },
3929
3640
 
@@ -3992,16 +3703,16 @@ function stateModelFactory({
3992
3703
  /**
3993
3704
  * #getter
3994
3705
  * whether the overlay marks each domain with a bar under its row instead
3995
- * of filling the row behind the letters. Letter-color mode hands the
3996
- * background to the color scheme, so a filled box would paint over it and
3997
- * leave the setting with nothing to show. Sub-row layout already stacks
3706
+ * of filling the row behind the letters: whenever the domains do not own
3707
+ * the cell fill (`cellFill`), a filled box would paint over the residue
3708
+ * colors the user picked. Sub-row layout already stacks
3998
3709
  * the boxes clear of the letters, and with the letters too small to draw
3999
3710
  * the filled box is the only thing left to read.
4000
3711
  */
4001
3712
  get domainUnderline() {
4002
3713
  return (
4003
3714
  self.actuallyShowDomains &&
4004
- !self.bgColor &&
3715
+ self.cellFill !== 'domain' &&
4005
3716
  !self.subFeatureRows &&
4006
3717
  self.showMsaLetters
4007
3718
  )
@@ -4523,6 +4234,12 @@ function stateModelFactory({
4523
4234
  setHideHeader(arg: boolean) {
4524
4235
  self.hideHeader = arg
4525
4236
  },
4237
+ /**
4238
+ * #action
4239
+ */
4240
+ setEmbeddedMode(arg: boolean) {
4241
+ self.embeddedMode = arg
4242
+ },
4526
4243
  /**
4527
4244
  * #action
4528
4245
  */
@@ -4602,14 +4319,15 @@ function stateModelFactory({
4602
4319
  },
4603
4320
  /**
4604
4321
  * #action
4605
- * show or hide an annotation type. Only hidden types are recorded; see
4606
- * `turnedOffFeatures`
4322
+ * show or hide an annotation type. Only a choice that differs from the
4323
+ * type's default is recorded; see `turnedOffFeatures`
4607
4324
  */
4608
4325
  setFilter(accession: string, shown: boolean) {
4609
- if (shown) {
4610
- self.turnedOffFeatures.delete(accession)
4326
+ // only a departure from the accession's default is recorded
4327
+ if (shown === self.defaultOffAccessions.has(accession)) {
4328
+ self.turnedOffFeatures.set(accession, !shown)
4611
4329
  } else {
4612
- self.turnedOffFeatures.set(accession, true)
4330
+ self.turnedOffFeatures.delete(accession)
4613
4331
  }
4614
4332
  },
4615
4333
 
@@ -4618,23 +4336,28 @@ function stateModelFactory({
4618
4336
  */
4619
4337
  fit() {
4620
4338
  // two passes: each direction's viewport depends on whether the other
4621
- // overflows (the minimap takes height, the vertical scrollbar width)
4339
+ // overflows (the minimap takes height, the vertical scrollbar width).
4340
+ // Fit shrinks an alignment into the window but never grows one past
4341
+ // the default cell size, which would stretch a small alignment's rows
4342
+ // and columns out of proportion
4622
4343
  transaction(() => {
4623
4344
  for (let pass = 0; pass < 2; pass++) {
4624
- this.fitHorizontally()
4625
- this.fitVertically()
4345
+ this.fitHorizontally(defaultColWidth)
4346
+ this.fitVertically(defaultRowHeight)
4626
4347
  }
4627
4348
  })
4628
4349
  },
4629
4350
  /**
4630
4351
  * #action
4631
4352
  */
4632
- fitVertically() {
4353
+ fitVertically(maxRowHeight = maxCellSize) {
4633
4354
  if (self.numRows > 0) {
4634
- self.rowHeight = clamp(
4635
- self.msaAreaHeight / self.numRows,
4636
- minRowHeight,
4637
- maxCellSize,
4355
+ self.setRowHeight(
4356
+ clamp(
4357
+ self.msaAreaHeight / self.numRows,
4358
+ minRowHeight,
4359
+ maxRowHeight,
4360
+ ),
4638
4361
  )
4639
4362
  }
4640
4363
  self.scrollY = 0
@@ -4651,10 +4374,12 @@ function stateModelFactory({
4651
4374
  return
4652
4375
  }
4653
4376
  transaction(() => {
4654
- self.colWidth = clamp(
4655
- self.msaCanvasWidth / (span.endCol - span.startCol + 1),
4656
- minColWidth,
4657
- maxCellSize,
4377
+ self.setColWidth(
4378
+ clamp(
4379
+ self.msaCanvasWidth / (span.endCol - span.startCol + 1),
4380
+ minColWidth,
4381
+ maxCellSize,
4382
+ ),
4658
4383
  )
4659
4384
  self.scrollX = clamp(
4660
4385
  -span.startCol * self.colWidth,
@@ -4700,10 +4425,12 @@ function stateModelFactory({
4700
4425
  const { rowRuns } = resolvedSelection
4701
4426
  const first = rowRuns[0]![0]
4702
4427
  const last = rowRuns.at(-1)![1]
4703
- self.rowHeight = clamp(
4704
- self.msaAreaHeight / (last - first + 1),
4705
- minRowHeight,
4706
- maxCellSize,
4428
+ self.setRowHeight(
4429
+ clamp(
4430
+ self.msaAreaHeight / (last - first + 1),
4431
+ minRowHeight,
4432
+ maxCellSize,
4433
+ ),
4707
4434
  )
4708
4435
  self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
4709
4436
  }
@@ -4744,13 +4471,15 @@ function stateModelFactory({
4744
4471
  /**
4745
4472
  * #action
4746
4473
  */
4747
- fitHorizontally() {
4474
+ fitHorizontally(maxColWidth = maxCellSize) {
4748
4475
  if (self.numColumns > 0) {
4749
4476
  // msaCanvasWidth excludes the vertical scrollbar's 20px
4750
- self.colWidth = clamp(
4751
- self.msaCanvasWidth / self.numColumns,
4752
- minColWidth,
4753
- maxCellSize,
4477
+ self.setColWidth(
4478
+ clamp(
4479
+ self.msaCanvasWidth / self.numColumns,
4480
+ minColWidth,
4481
+ maxColWidth,
4482
+ ),
4754
4483
  )
4755
4484
  }
4756
4485
  self.scrollX = 0
@@ -4761,21 +4490,18 @@ function stateModelFactory({
4761
4490
  self.setHighlightedColumns(self.highlightColumns)
4762
4491
  }
4763
4492
 
4764
- // The `collapse`, `focus` and `rotate` clade marks seed the collapsed
4765
- // list, the subtree in focus and the rotated nodes, which the tree,
4766
- // `hideGapsEffective` and the alignment all read. The tree arrives with
4767
- // the model for inline data and later for a filehandle, so the seeding
4768
- // waits for it and then runs once per data load, per change of
4769
- // `clades` and per reroot, which clears the node ids the seeding
4770
- // wrote: expanding a seeded clade sticks until one of those changes.
4771
- // `dataInitialized` is true once the MSA alone has loaded, when the
4772
- // tree is still the flat stub, so a tree filehandle holds the seeding
4773
- // until its text lands.
4493
+ const awaitingTree = () => !!self.treeFilehandle && !self.data.tree
4494
+
4495
+ // `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
4496
+ // and `rotated`. Seeding runs once per data load, `clades` change and
4497
+ // reroot, so expanding a seeded clade sticks until one of those. A tree
4498
+ // filehandle holds it back, since `dataInitialized` is true while the
4499
+ // tree is still the flat stub.
4774
4500
  addDisposer(
4775
4501
  self,
4776
4502
  reaction(
4777
4503
  () =>
4778
- self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
4504
+ self.dataInitialized && !awaitingTree()
4779
4505
  ? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
4780
4506
  : undefined,
4781
4507
  seed => {
@@ -4813,7 +4539,7 @@ function stateModelFactory({
4813
4539
  region &&
4814
4540
  self.viewInitialized &&
4815
4541
  self.numColumns > 0 &&
4816
- !(self.treeFilehandle && !self.data.tree)
4542
+ !awaitingTree()
4817
4543
  ) {
4818
4544
  this.revealRegion(region)
4819
4545
  self.setRegion(undefined)
@@ -4842,107 +4568,7 @@ function stateModelFactory({
4842
4568
  })
4843
4569
  }
4844
4570
 
4845
- /**
4846
- * Fetch a filehandle whenever it changes, and hand the text to
4847
- * `onLoad`.
4848
- *
4849
- * A generation guard keeps a slower superseded request from overwriting
4850
- * the newer one's data, status or loading flag. A superseded or cleared
4851
- * request is aborted and clears its status line.
4852
- *
4853
- * `clearFilehandle` runs after a local file loads, since a blob has no
4854
- * URL to refetch, and when the user cancels, which returns the view to
4855
- * the import form.
4856
- *
4857
- * `what` names the layer in a failure message. An `optional` layer
4858
- * (annotations, row metadata) that fails adds a warning instead of an
4859
- * error.
4860
- */
4861
- const loadOnFilehandleChange = ({
4862
- what,
4863
- getFilehandle,
4864
- onLoad,
4865
- setLoading,
4866
- clearFilehandle,
4867
- optional = false,
4868
- }: {
4869
- what: string
4870
- getFilehandle: () => FileLocationType | undefined
4871
- onLoad: (text: string) => void
4872
- setLoading?: (arg: boolean) => void
4873
- clearFilehandle?: () => void
4874
- optional?: boolean
4875
- }) => {
4876
- let generation = 0
4877
- let cancelInFlight: (() => void) | undefined
4878
- addDisposer(self, () => {
4879
- cancelInFlight?.()
4880
- })
4881
- addDisposer(
4882
- self,
4883
- autorun(async () => {
4884
- const filehandle = getFilehandle()
4885
- const current = ++generation
4886
- cancelInFlight?.()
4887
- cancelInFlight = undefined
4888
- if (!filehandle) {
4889
- setLoading?.(false)
4890
- return
4891
- }
4892
- const isCurrent = () => current === generation && isAlive(self)
4893
- const controller = new AbortController()
4894
- cancelInFlight = () => {
4895
- controller.abort()
4896
- if (isAlive(self)) {
4897
- self.setStatus(undefined)
4898
- }
4899
- }
4900
- try {
4901
- setLoading?.(true)
4902
- if (!optional) {
4903
- self.setError(undefined)
4904
- }
4905
- const text = await fetchTextWithProgress(
4906
- openLocation(filehandle),
4907
- status => {
4908
- if (isCurrent()) {
4909
- self.setStatus(status)
4910
- }
4911
- },
4912
- { controller },
4913
- )
4914
- if (isCurrent()) {
4915
- transaction(() => {
4916
- onLoad(text)
4917
- if (filehandle.locationType === 'BlobLocation') {
4918
- clearFilehandle?.()
4919
- }
4920
- })
4921
- }
4922
- } catch (e) {
4923
- if (isCurrent()) {
4924
- if (isAbortError(e)) {
4925
- clearFilehandle?.()
4926
- } else {
4927
- console.error(e)
4928
- if (optional) {
4929
- self.addWarning(`The ${what} did not load: ${e}`)
4930
- } else {
4931
- self.setError(e)
4932
- }
4933
- }
4934
- }
4935
- } finally {
4936
- if (isCurrent()) {
4937
- cancelInFlight = undefined
4938
- setLoading?.(false)
4939
- }
4940
- }
4941
- }),
4942
- )
4943
- }
4944
-
4945
- loadOnFilehandleChange({
4571
+ installFilehandleLoader(self, {
4946
4572
  what: 'tree',
4947
4573
  getFilehandle: () => self.treeFilehandle,
4948
4574
  onLoad: text => {
@@ -4957,7 +4583,7 @@ function stateModelFactory({
4957
4583
  })
4958
4584
 
4959
4585
  // treeMetadata has no import-form step, so no loading flag or cancel
4960
- loadOnFilehandleChange({
4586
+ installFilehandleLoader(self, {
4961
4587
  what: 'row metadata',
4962
4588
  optional: true,
4963
4589
  getFilehandle: () => self.treeMetadataFilehandle,
@@ -4989,7 +4615,7 @@ function stateModelFactory({
4989
4615
  )
4990
4616
 
4991
4617
  // gffFilehandle loads into data.gff, which the autorun above parses
4992
- loadOnFilehandleChange({
4618
+ installFilehandleLoader(self, {
4993
4619
  what: 'annotations',
4994
4620
  optional: true,
4995
4621
  getFilehandle: () => self.gffFilehandle,
@@ -5001,7 +4627,7 @@ function stateModelFactory({
5001
4627
  },
5002
4628
  })
5003
4629
 
5004
- loadOnFilehandleChange({
4630
+ installFilehandleLoader(self, {
5005
4631
  what: 'alignment',
5006
4632
  getFilehandle: () => self.msaFilehandle,
5007
4633
  onLoad: text => {
@@ -5025,20 +4651,16 @@ function stateModelFactory({
5025
4651
  self,
5026
4652
  autorun(() => {
5027
4653
  if (self.dynamicColorSchemeName) {
5028
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
5029
- self.colStats
4654
+ readComputed(self.colStats)
5030
4655
  }
5031
4656
  if (self.showColumnStats) {
5032
- // everything the hover tooltip reads per column
5033
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
5034
- self.colStats
5035
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
5036
- self.conservation
5037
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
5038
- self.propertyConservation
4657
+ readComputed(
4658
+ self.colStats,
4659
+ self.conservation,
4660
+ self.propertyConservation,
4661
+ )
5039
4662
  }
5040
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
5041
- self.columns
4663
+ readComputed(self.columns)
5042
4664
  }),
5043
4665
  )
5044
4666
  },