react-msaview 9.0.0 → 9.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +111 -115
  3. package/bundle/index.js.map +4 -4
  4. package/dist/blosum62.d.ts +3 -0
  5. package/dist/blosum62.js +43 -0
  6. package/dist/blosum62.js.map +1 -0
  7. package/dist/colorSchemes.js +2 -3
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/columnCounts.d.ts +0 -1
  10. package/dist/columnCounts.js +1 -1
  11. package/dist/columnCounts.js.map +1 -1
  12. package/dist/components/Loading.d.ts +3 -2
  13. package/dist/components/Loading.js +7 -7
  14. package/dist/components/Loading.js.map +1 -1
  15. package/dist/components/MSAView.d.ts +3 -2
  16. package/dist/components/MSAView.js +7 -4
  17. package/dist/components/MSAView.js.map +1 -1
  18. package/dist/components/MSAViewer.d.ts +1941 -4
  19. package/dist/components/MSAViewer.js +144 -201
  20. package/dist/components/MSAViewer.js.map +1 -1
  21. package/dist/components/MetadataTable.d.ts +4 -1
  22. package/dist/components/MetadataTable.js +55 -16
  23. package/dist/components/MetadataTable.js.map +1 -1
  24. package/dist/components/ResizeHandles.js +3 -2
  25. package/dist/components/ResizeHandles.js.map +1 -1
  26. package/dist/components/Track.js +8 -1
  27. package/dist/components/Track.js.map +1 -1
  28. package/dist/components/controls.d.ts +11 -0
  29. package/dist/components/controls.js +49 -0
  30. package/dist/components/controls.js.map +1 -0
  31. package/dist/components/dialogs/AnnotationFileDialog.js +12 -18
  32. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  33. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  34. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  35. package/dist/components/dialogs/FeatureDialog.js +2 -2
  36. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  37. package/dist/components/{header/TreeSettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
  38. package/dist/components/dialogs/GapThresholdDialog.js +15 -0
  39. package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
  40. package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
  41. package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
  42. package/dist/components/dialogs/GappynessSlider.js.map +1 -0
  43. package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
  44. package/dist/components/dialogs/GoToBox.js +48 -0
  45. package/dist/components/dialogs/GoToBox.js.map +1 -0
  46. package/dist/components/dialogs/GoToDialog.d.ts +6 -0
  47. package/dist/components/dialogs/GoToDialog.js +10 -0
  48. package/dist/components/dialogs/GoToDialog.js.map +1 -0
  49. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  50. package/dist/components/dialogs/HelpDialog.js +30 -0
  51. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  52. package/dist/components/dialogs/MetadataDialog.js +11 -3
  53. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  54. package/dist/components/dialogs/goTo.d.ts +5 -0
  55. package/dist/components/{header → dialogs}/goTo.js +1 -1
  56. package/dist/components/dialogs/goTo.js.map +1 -0
  57. package/dist/components/dialogs/metadataLabels.d.ts +4 -0
  58. package/dist/components/dialogs/metadataLabels.js +52 -0
  59. package/dist/components/dialogs/metadataLabels.js.map +1 -0
  60. package/dist/components/dialogs/metadataRecord.d.ts +1 -0
  61. package/dist/components/dialogs/metadataRecord.js +38 -0
  62. package/dist/components/dialogs/metadataRecord.js.map +1 -0
  63. package/dist/components/header/Header.d.ts +6 -2
  64. package/dist/components/header/Header.js +30 -34
  65. package/dist/components/header/Header.js.map +1 -1
  66. package/dist/components/header/HiddenColumnsChip.d.ts +6 -0
  67. package/dist/components/header/HiddenColumnsChip.js +15 -0
  68. package/dist/components/header/HiddenColumnsChip.js.map +1 -0
  69. package/dist/components/header/{FileMenu.d.ts → MenuBar.d.ts} +3 -3
  70. package/dist/components/header/MenuBar.js +33 -0
  71. package/dist/components/header/MenuBar.js.map +1 -0
  72. package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
  73. package/dist/components/header/ScrollZoomCheckbox.js +19 -0
  74. package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
  75. package/dist/components/header/ZoomControls.js +12 -48
  76. package/dist/components/header/ZoomControls.js.map +1 -1
  77. package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
  78. package/dist/components/header/colorSchemeMenuItems.js +106 -0
  79. package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
  80. package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
  81. package/dist/components/header/getAnnotationsMenu.js +48 -54
  82. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  83. package/dist/components/header/menuBarItems.d.ts +11 -0
  84. package/dist/components/header/menuBarItems.js +277 -0
  85. package/dist/components/header/menuBarItems.js.map +1 -0
  86. package/dist/components/import/ImportForm.js +12 -6
  87. package/dist/components/import/ImportForm.js.map +1 -1
  88. package/dist/components/import/data/seq2.d.ts +0 -1
  89. package/dist/components/import/data/seq2.js +0 -17
  90. package/dist/components/import/data/seq2.js.map +1 -1
  91. package/dist/components/minimap/Minimap.js +7 -3
  92. package/dist/components/minimap/Minimap.js.map +1 -1
  93. package/dist/components/minimap/MinimapSVG.js +2 -1
  94. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  95. package/dist/components/minimap/minimapLayout.d.ts +12 -0
  96. package/dist/components/minimap/minimapLayout.js +20 -0
  97. package/dist/components/minimap/minimapLayout.js.map +1 -1
  98. package/dist/components/msa/MSACanvas.js +28 -10
  99. package/dist/components/msa/MSACanvas.js.map +1 -1
  100. package/dist/components/msa/MSACanvasBlock.js +17 -16
  101. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  102. package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
  103. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  104. package/dist/components/msa/WheelZoomHint.d.ts +7 -0
  105. package/dist/components/msa/WheelZoomHint.js +38 -0
  106. package/dist/components/msa/WheelZoomHint.js.map +1 -0
  107. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  108. package/dist/components/msa/drawFeatureSpans.js +99 -56
  109. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  110. package/dist/components/msa/msaKeys.js +3 -0
  111. package/dist/components/msa/msaKeys.js.map +1 -1
  112. package/dist/components/msa/msaRaster.d.ts +14 -2
  113. package/dist/components/msa/msaRaster.js +20 -10
  114. package/dist/components/msa/msaRaster.js.map +1 -1
  115. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  116. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +39 -8
  117. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  118. package/dist/components/msa/renderHighlights.d.ts +2 -5
  119. package/dist/components/msa/renderHighlights.js +5 -6
  120. package/dist/components/msa/renderHighlights.js.map +1 -1
  121. package/dist/components/msa/renderMSABlock.d.ts +6 -1
  122. package/dist/components/msa/renderMSABlock.js +44 -17
  123. package/dist/components/msa/renderMSABlock.js.map +1 -1
  124. package/dist/components/msa/renderMSAMouseover.js +2 -1
  125. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  126. package/dist/components/msa/tileColor.js +7 -0
  127. package/dist/components/msa/tileColor.js.map +1 -1
  128. package/dist/components/renderCtx.d.ts +1 -1
  129. package/dist/components/rowpanels/RowPanelBlock.js +2 -2
  130. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  131. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  132. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  133. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  134. package/dist/components/settings/MenuItemsForm.d.ts +10 -0
  135. package/dist/components/settings/MenuItemsForm.js +80 -0
  136. package/dist/components/settings/MenuItemsForm.js.map +1 -0
  137. package/dist/components/settings/SettingsPanel.d.ts +12 -0
  138. package/dist/components/settings/SettingsPanel.js +92 -0
  139. package/dist/components/settings/SettingsPanel.js.map +1 -0
  140. package/dist/components/{header/ZoomMenu.d.ts → settings/TrackList.d.ts} +2 -2
  141. package/dist/components/settings/TrackList.js +55 -0
  142. package/dist/components/settings/TrackList.js.map +1 -0
  143. package/dist/components/tracks/TrackBlocks.js +2 -2
  144. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  145. package/dist/components/tracks/drawTracks.d.ts +1 -53
  146. package/dist/components/tracks/drawTracks.js +5 -5
  147. package/dist/components/tracks/drawTracks.js.map +1 -1
  148. package/dist/components/tree/CladeLabels.d.ts +1 -1
  149. package/dist/components/tree/CladeLabels.js +22 -34
  150. package/dist/components/tree/CladeLabels.js.map +1 -1
  151. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  152. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  153. package/dist/components/tree/TreeMenu.js +4 -6
  154. package/dist/components/tree/TreeMenu.js.map +1 -1
  155. package/dist/components/tree/TreeOverview.js +2 -2
  156. package/dist/components/tree/TreeOverview.js.map +1 -1
  157. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  158. package/dist/components/tree/cladeBrackets.js +33 -3
  159. package/dist/components/tree/cladeBrackets.js.map +1 -1
  160. package/dist/components/tree/renderTreeCanvas.js +2 -8
  161. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  162. package/dist/components/tree/renderTreeOverview.js +4 -11
  163. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  164. package/dist/constants.d.ts +4 -1
  165. package/dist/constants.js +7 -1
  166. package/dist/constants.js.map +1 -1
  167. package/dist/darkReader.d.ts +7 -0
  168. package/dist/darkReader.js +42 -0
  169. package/dist/darkReader.js.map +1 -0
  170. package/dist/element.js +25 -53
  171. package/dist/element.js.map +1 -1
  172. package/dist/exportLayout.d.ts +39 -0
  173. package/dist/exportLayout.js +78 -0
  174. package/dist/exportLayout.js.map +1 -0
  175. package/dist/featurePanels.d.ts +37 -0
  176. package/dist/featurePanels.js +187 -0
  177. package/dist/featurePanels.js.map +1 -0
  178. package/dist/filehandleLoader.d.ts +41 -0
  179. package/dist/filehandleLoader.js +99 -0
  180. package/dist/filehandleLoader.js.map +1 -0
  181. package/dist/headlessRenderEnv.d.ts +12 -39
  182. package/dist/headlessRenderEnv.js +18 -12
  183. package/dist/headlessRenderEnv.js.map +1 -1
  184. package/dist/hierarchy.d.ts +5 -8
  185. package/dist/hierarchy.js +5 -79
  186. package/dist/hierarchy.js.map +1 -1
  187. package/dist/index.d.ts +6 -1
  188. package/dist/index.js +6 -1
  189. package/dist/index.js.map +1 -1
  190. package/dist/measureTextCanvas.d.ts +7 -0
  191. package/dist/measureTextCanvas.js +8 -6
  192. package/dist/measureTextCanvas.js.map +1 -1
  193. package/dist/model/treeModel.d.ts +6 -2
  194. package/dist/model/treeModel.js +8 -4
  195. package/dist/model/treeModel.js.map +1 -1
  196. package/dist/model.d.ts +180 -76
  197. package/dist/model.js +358 -618
  198. package/dist/model.js.map +1 -1
  199. package/dist/neighborJoining.js +1 -40
  200. package/dist/neighborJoining.js.map +1 -1
  201. package/dist/renderToSvg.d.ts +2 -7
  202. package/dist/renderToSvg.js +33 -133
  203. package/dist/renderToSvg.js.map +1 -1
  204. package/dist/rerootTree.d.ts +1 -0
  205. package/dist/rerootTree.js +7 -6
  206. package/dist/rerootTree.js.map +1 -1
  207. package/dist/schemeResidueColor.d.ts +12 -0
  208. package/dist/schemeResidueColor.js +33 -0
  209. package/dist/schemeResidueColor.js.map +1 -0
  210. package/dist/similarityColors.d.ts +11 -0
  211. package/dist/similarityColors.js +90 -0
  212. package/dist/similarityColors.js.map +1 -0
  213. package/dist/svgTestUtil.d.ts +74 -32
  214. package/dist/treeDisplay.d.ts +37 -0
  215. package/dist/treeDisplay.js +209 -0
  216. package/dist/treeDisplay.js.map +1 -0
  217. package/dist/types.d.ts +6 -3
  218. package/dist/useWheelScroll.d.ts +3 -1
  219. package/dist/useWheelScroll.js +36 -17
  220. package/dist/useWheelScroll.js.map +1 -1
  221. package/dist/util.d.ts +2 -0
  222. package/dist/util.js +2 -0
  223. package/dist/util.js.map +1 -1
  224. package/dist/version.d.ts +1 -1
  225. package/dist/version.js +1 -1
  226. package/dist/viewerSnapshot.d.ts +169 -0
  227. package/dist/viewerSnapshot.js +100 -0
  228. package/dist/viewerSnapshot.js.map +1 -0
  229. package/dist/wheelZoom.d.ts +8 -3
  230. package/dist/wheelZoom.js +44 -32
  231. package/dist/wheelZoom.js.map +1 -1
  232. package/dist/zoomSnapshot.d.ts +29 -0
  233. package/dist/zoomSnapshot.js +47 -0
  234. package/dist/zoomSnapshot.js.map +1 -0
  235. package/package.json +11 -11
  236. package/src/blosum62.ts +45 -0
  237. package/src/colorSchemes.ts +2 -4
  238. package/src/columnCounts.ts +1 -1
  239. package/src/components/Loading.tsx +25 -14
  240. package/src/components/MSAView.tsx +22 -5
  241. package/src/components/MSAViewer.tsx +275 -251
  242. package/src/components/MetadataTable.tsx +83 -26
  243. package/src/components/ResizeHandles.tsx +9 -2
  244. package/src/components/Track.tsx +15 -1
  245. package/src/components/controls.ts +62 -0
  246. package/src/components/dialogs/AnnotationFileDialog.tsx +16 -21
  247. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  248. package/src/components/dialogs/FeatureDialog.tsx +2 -5
  249. package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
  250. package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
  251. package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
  252. package/src/components/dialogs/GoToDialog.tsx +30 -0
  253. package/src/components/dialogs/HelpDialog.tsx +79 -0
  254. package/src/components/dialogs/MetadataDialog.tsx +32 -4
  255. package/src/components/{header → dialogs}/goTo.ts +1 -1
  256. package/src/components/dialogs/metadataLabels.ts +54 -0
  257. package/src/components/dialogs/metadataRecord.tsx +61 -0
  258. package/src/components/header/Header.tsx +75 -44
  259. package/src/components/header/HiddenColumnsChip.tsx +39 -0
  260. package/src/components/header/MenuBar.tsx +64 -0
  261. package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
  262. package/src/components/header/ZoomControls.tsx +23 -74
  263. package/src/components/header/colorSchemeMenuItems.ts +121 -0
  264. package/src/components/header/getAnnotationsMenu.ts +52 -56
  265. package/src/components/header/menuBarItems.ts +357 -0
  266. package/src/components/import/ImportForm.tsx +19 -7
  267. package/src/components/import/data/seq2.ts +0 -18
  268. package/src/components/minimap/Minimap.tsx +11 -3
  269. package/src/components/minimap/MinimapSVG.tsx +17 -1
  270. package/src/components/minimap/minimapLayout.ts +27 -0
  271. package/src/components/msa/MSACanvas.tsx +24 -4
  272. package/src/components/msa/MSACanvasBlock.tsx +20 -17
  273. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
  274. package/src/components/msa/WheelZoomHint.tsx +56 -0
  275. package/src/components/msa/drawFeatureSpans.ts +160 -79
  276. package/src/components/msa/msaKeys.ts +3 -0
  277. package/src/components/msa/msaRaster.ts +26 -10
  278. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +62 -24
  279. package/src/components/msa/renderHighlights.ts +5 -6
  280. package/src/components/msa/renderMSABlock.ts +51 -15
  281. package/src/components/msa/renderMSAMouseover.ts +5 -1
  282. package/src/components/msa/tileColor.ts +7 -0
  283. package/src/components/renderCtx.ts +0 -1
  284. package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
  285. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  286. package/src/components/settings/MenuItemsForm.tsx +225 -0
  287. package/src/components/settings/SettingsPanel.tsx +156 -0
  288. package/src/components/settings/TrackList.tsx +119 -0
  289. package/src/components/tracks/TrackBlocks.tsx +2 -2
  290. package/src/components/tracks/drawTracks.ts +5 -5
  291. package/src/components/tree/CladeLabels.tsx +30 -54
  292. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  293. package/src/components/tree/TreeMenu.tsx +4 -6
  294. package/src/components/tree/TreeOverview.tsx +2 -2
  295. package/src/components/tree/cladeBrackets.ts +50 -3
  296. package/src/components/tree/renderTreeCanvas.ts +2 -13
  297. package/src/components/tree/renderTreeOverview.ts +7 -14
  298. package/src/constants.ts +9 -1
  299. package/src/darkReader.ts +57 -0
  300. package/src/element.ts +37 -64
  301. package/src/exportLayout.ts +134 -0
  302. package/src/featurePanels.ts +269 -0
  303. package/src/filehandleLoader.ts +136 -0
  304. package/src/headlessRenderEnv.ts +24 -11
  305. package/src/hierarchy.ts +5 -88
  306. package/src/index.ts +9 -5
  307. package/src/measureTextCanvas.ts +8 -6
  308. package/src/model/treeModel.ts +9 -3
  309. package/src/model.ts +452 -807
  310. package/src/neighborJoining.ts +1 -42
  311. package/src/renderToSvg.tsx +56 -207
  312. package/src/rerootTree.ts +7 -6
  313. package/src/schemeResidueColor.ts +37 -0
  314. package/src/similarityColors.ts +112 -0
  315. package/src/treeDisplay.ts +278 -0
  316. package/src/types.ts +6 -4
  317. package/src/useWheelScroll.ts +43 -22
  318. package/src/util.ts +3 -0
  319. package/src/version.ts +1 -1
  320. package/src/viewerSnapshot.ts +147 -0
  321. package/src/wheelZoom.ts +55 -33
  322. package/src/zoomSnapshot.ts +80 -0
  323. package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
  324. package/dist/components/header/ColorSchemeMenu.js +0 -37
  325. package/dist/components/header/ColorSchemeMenu.js.map +0 -1
  326. package/dist/components/header/FileMenu.js +0 -66
  327. package/dist/components/header/FileMenu.js.map +0 -1
  328. package/dist/components/header/GappynessSlider.js.map +0 -1
  329. package/dist/components/header/GoToBox.js +0 -31
  330. package/dist/components/header/GoToBox.js.map +0 -1
  331. package/dist/components/header/MSASettingsMenu.d.ts +0 -6
  332. package/dist/components/header/MSASettingsMenu.js +0 -11
  333. package/dist/components/header/MSASettingsMenu.js.map +0 -1
  334. package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
  335. package/dist/components/header/ScrollZoomIcons.js +0 -35
  336. package/dist/components/header/ScrollZoomIcons.js.map +0 -1
  337. package/dist/components/header/TreeSettingsMenu.js +0 -42
  338. package/dist/components/header/TreeSettingsMenu.js.map +0 -1
  339. package/dist/components/header/ZoomMenu.js +0 -37
  340. package/dist/components/header/ZoomMenu.js.map +0 -1
  341. package/dist/components/header/goTo.d.ts +0 -20
  342. package/dist/components/header/goTo.js.map +0 -1
  343. package/dist/components/header/settingsMenuItems.d.ts +0 -4
  344. package/dist/components/header/settingsMenuItems.js +0 -127
  345. package/dist/components/header/settingsMenuItems.js.map +0 -1
  346. package/src/components/header/ColorSchemeMenu.tsx +0 -56
  347. package/src/components/header/FileMenu.tsx +0 -81
  348. package/src/components/header/MSASettingsMenu.tsx +0 -26
  349. package/src/components/header/ScrollZoomIcons.tsx +0 -52
  350. package/src/components/header/TreeSettingsMenu.tsx +0 -60
  351. package/src/components/header/ZoomMenu.tsx +0 -48
  352. package/src/components/header/settingsMenuItems.ts +0 -157
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,11 +17,8 @@ import {
19
17
  import {
20
18
  generateNodeIds,
21
19
  gffToAnnotations,
22
- interProScanToAnnotations,
23
- parseEmfTree,
24
20
  parseGFF,
25
21
  parseMSA,
26
- parseNewick,
27
22
  } from 'msa-parsers'
28
23
 
29
24
  import { blockFasta } from './blockFasta.ts'
@@ -34,18 +29,14 @@ import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
34
29
  import { columnStats } from './columnStats.ts'
35
30
  import { packDomainLanes } from './components/msa/packDomainLanes.ts'
36
31
  import { visibleColRange } from './components/msa/visibleColRange.ts'
37
- import TrackBlocks from './components/tracks/TrackBlocks.tsx'
38
32
  import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
39
33
  import { scaleBarLength } from './components/tree/scaleBar.ts'
40
34
  import {
41
- cladeHighlightAlpha,
42
- cladeHighlightColor,
43
35
  defaultAllowedGappyness,
44
36
  defaultColWidth,
45
37
  defaultColorSchemeName,
46
38
  defaultCurrentAlignment,
47
39
  defaultDrawMsaLetters,
48
- defaultFeaturePanelWidth,
49
40
  defaultHeight,
50
41
  defaultHideGaps,
51
42
  defaultRowHeight,
@@ -74,39 +65,37 @@ import {
74
65
  scrollZoomAxes,
75
66
  segmentFeatureTypes,
76
67
  segmentShades,
68
+ settingsPanelWidth,
77
69
  treeScaleBarHeight,
78
70
  } from './constants.ts'
79
71
  import { createPaletteMap } from './createPaletteMap.ts'
80
72
  import { exportFileName } from './exportFileName.ts'
81
- 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'
82
83
  import { featuresToAnnotations } from './featuresLayer.ts'
83
- import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
84
- import { flatToTree } from './flatToTree.ts'
84
+ import { installFilehandleLoader } from './filehandleLoader.ts'
85
85
  import {
86
- calcDepthToLeaf,
87
- clusterLayout,
88
- collapse,
89
86
  find,
90
- forEachDescendant,
91
87
  hierarchy,
92
88
  layoutTree,
93
- leafIndex,
94
89
  leaves,
95
- maxLength,
96
- mrca,
97
90
  nodeCoveringRows,
98
- setBrLength,
99
- sort,
100
- sum as hierarchySum,
101
91
  } from './hierarchy.ts'
102
92
  import { DataModelF } from './model/DataModel.ts'
103
93
  import { DialogQueueSessionMixin } from './model/DialogQueue.ts'
104
94
  import { MSAModelF } from './model/msaModel.ts'
105
95
  import { TreeModelF } from './model/treeModel.ts'
106
96
  import { calculateNeighborJoiningTree } from './neighborJoining.ts'
107
- import { parseAsn1 } from './parseAsn1.ts'
108
97
  import { calculatePropertyConservation } from './propertyConservation.ts'
109
- import { midpointRoot, rerootTree } from './rerootTree.ts'
98
+ import { preorder } from './rerootTree.ts'
110
99
  import {
111
100
  globalColToVisibleCol,
112
101
  seqPosOfGlobalCol,
@@ -116,33 +105,43 @@ import {
116
105
  import { resolveScale } from './scales.ts'
117
106
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
118
107
  import { maxBitsFor } from './sequenceLogo.ts'
108
+ import {
109
+ blosum62ColumnColors,
110
+ identityColumnColors,
111
+ } from './similarityColors.ts'
119
112
  import { stripDefault } from './stripDefault.ts'
120
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'
121
122
  import {
122
123
  computeRowInsertions,
123
124
  dropBlanks,
124
125
  len,
125
- outlineColor,
126
+ readComputed,
126
127
  skipBlanks,
127
128
  withAlpha,
128
129
  } from './util.ts'
129
130
  import { saveAs } from './vendor/fileSaver.ts'
130
131
  import { writeNewick } from './writeNewick.ts'
131
132
  import { parseWuss } from './wuss.ts'
133
+ import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.ts'
132
134
 
133
135
  import type { ColumnStats } from './columnStats.ts'
134
136
  import type { ScrollZoomAxis } from './constants.ts'
135
137
  import type { HierarchyNode } from './hierarchy.ts'
136
138
  import type { ExportSvgOptions } from './renderToSvg.tsx'
137
- import type { TreeNode } from './rerootTree.ts'
138
- import type { ScaleSpec } from './scales.ts'
139
139
  import type {
140
140
  Annotation,
141
141
  Arc,
142
142
  BasicTrack,
143
143
  Cell,
144
144
  Clade,
145
- CladeMark,
146
145
  ResidueMappingProblem,
147
146
  ColumnTrackSpec,
148
147
  DomainBand,
@@ -160,14 +159,11 @@ import type {
160
159
  ResolvedClade,
161
160
  ResolvedEncoding,
162
161
  ResolvedFeaturePanel,
162
+ ResolvedStripPanel,
163
163
  ResolvedHighlight,
164
164
  ResolvedSelection,
165
- TreeOrder,
166
165
  TreeRoot,
167
166
  ResolvedRowPanel,
168
- ResolvedStripPanel,
169
- RowFeaturesSpec,
170
- RowPanelSpan,
171
167
  RowDataInput,
172
168
  RowPanelSpec,
173
169
  RowResidue,
@@ -176,17 +172,10 @@ import type {
176
172
  UnshareableData,
177
173
  Viewport,
178
174
  } from './types.ts'
175
+ import type { ZoomSnapshot } from './zoomSnapshot.ts'
179
176
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
180
177
  import type { Instance } from '@jbrowse/mobx-state-tree'
181
178
  import type { IComputedValue } from 'mobx'
182
- import type { InterProScanResults } from 'msa-parsers'
183
-
184
- function parseTreeText(text: string) {
185
- if (text.startsWith('BioTreeContainer')) {
186
- return flatToTree(parseAsn1(text))
187
- }
188
- return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
189
- }
190
179
 
191
180
  // The height each kind draws at before the user drags a divider. A text track
192
181
  // is absent: it is one alignment row tall and follows rowHeight, so the zoom
@@ -216,262 +205,6 @@ const ownHeightKey = (id: string) => `own:${id}`
216
205
  const HELIX_ARC = '#4e79a7'
217
206
  const PSEUDOKNOT_ARC = '#e15759'
218
207
 
219
- // the fill of a span under a scale that gives its value no color, such as a
220
- // gene a `map` over gene names leaves out
221
- const UNSCALED_FEATURE = '#d9d9d9'
222
-
223
- // the fraction of a span a following one may cover before it takes a lane of
224
- // its own in a `features` panel
225
- const laneOverlap = 0.1
226
-
227
- // a row panel's own width, or a default: the row height, which makes a strip
228
- // cell square, and a gene neighborhood's width for a features panel. The row
229
- // height is read only when used, so a panel with a width of its own does not
230
- // recompute on a vertical zoom
231
- function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
232
- return (
233
- panel.width ??
234
- (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
235
- )
236
- }
237
-
238
- // one row of the row table as the channels read it. A number or a boolean
239
- // reads as its string, the way a GFF value does, and null or an object drops
240
- function stringFields(row: unknown) {
241
- if (typeof row !== 'object' || row === null) {
242
- return undefined
243
- }
244
- const fields: Record<string, string> = {}
245
- for (const [field, value] of Object.entries(row)) {
246
- if (typeof value === 'string') {
247
- fields[field] = value
248
- } else if (typeof value === 'number' || typeof value === 'boolean') {
249
- fields[field] = String(value)
250
- }
251
- }
252
- return fields
253
- }
254
-
255
- // the color each row takes from a field of the row table under a scale
256
- function colorByRow(
257
- rowData: Record<string, Record<string, string> | undefined>,
258
- field: string,
259
- colorOf: (value: string) => string | undefined,
260
- ) {
261
- const colors = new Map<string, string>()
262
- for (const [name, row] of Object.entries(rowData)) {
263
- const value = row?.[field]
264
- const color = value === undefined ? undefined : colorOf(value)
265
- if (color) {
266
- colors.set(name, color)
267
- }
268
- }
269
- return colors
270
- }
271
-
272
- type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
273
- type FeaturePanelScale = Omit<
274
- ResolvedFeaturePanel,
275
- 'id' | 'width' | 'offsetX' | 'spans'
276
- >
277
-
278
- // the fill and outline of every feature in a list: its own GFF `color=` first,
279
- // then the scale an encoding resolves over one of its fields, then the
280
- // accession palette
281
- function featureColorMap(
282
- annotations: Annotation[],
283
- encoding:
284
- | { field: string; colorOf: (value: string) => string | undefined }
285
- | undefined,
286
- fillPalette: Record<string, string>,
287
- ) {
288
- const strokes = new Map<string, string>()
289
- const strokeOf = (fill: string) => {
290
- const hit = strokes.get(fill)
291
- if (hit !== undefined) {
292
- return hit
293
- }
294
- const stroke = outlineColor(fill)
295
- strokes.set(fill, stroke)
296
- return stroke
297
- }
298
- return new Map(
299
- annotations.map(annotation => {
300
- const value = encoding
301
- ? featureField(annotation, encoding.field)
302
- : undefined
303
- const fill =
304
- annotation.color ??
305
- (value === undefined ? undefined : encoding!.colorOf(value)) ??
306
- (encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
307
- return [annotation, { fill, stroke: strokeOf(fill) }]
308
- }),
309
- )
310
- }
311
-
312
- // the text a label channel draws inside each span, for the features carrying
313
- // the field it names
314
- function featureLabelMap(annotations: Annotation[], field: string) {
315
- const labels = new Map<Annotation, string>()
316
- for (const annotation of annotations) {
317
- const value = featureField(annotation, field)
318
- if (value !== undefined) {
319
- labels.set(annotation, value)
320
- }
321
- }
322
- return labels
323
- }
324
-
325
- /** what `packDomainLanes` needs to lay a row out, before it has its lane */
326
- type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
327
- startCol: number
328
- endCol: number
329
- }
330
-
331
- /**
332
- * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
333
- * forward features stack above the line and the reverse below it, each strand
334
- * packing on its own. The deepest row on each side sets the grid every row lays
335
- * out on, so the line between the strands sits at one height down the panel and
336
- * a reader can scan it. A feature with no strand piles with the forward ones.
337
- */
338
- function strandpileLanes(
339
- rows: [string, Unlaned[]][],
340
- ): Map<string, RowPanelSpan[]> {
341
- const piled = rows.map(([name, bands]) => {
342
- const reverse = packDomainLanes(
343
- bands.filter(b => b.annotation.strand === -1),
344
- )
345
- const forward = packDomainLanes(
346
- bands.filter(b => b.annotation.strand !== -1),
347
- )
348
- return { name, forward, reverse }
349
- })
350
- const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
351
- const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
352
- const laneCount = up + down
353
- return new Map(
354
- piled.map(({ name, forward, reverse }) => [
355
- name,
356
- [
357
- // forward level 0 is the lane just above the line, stacking upward
358
- ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
359
- ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
360
- ],
361
- ]),
362
- )
363
- }
364
-
365
- /** one row's spans laid out by the panel's `position` */
366
- function panelLanes(
367
- panel: RowFeaturesSpec,
368
- rows: [string, Unlaned[]][],
369
- ): Map<string, RowPanelSpan[]> {
370
- return panel.position === 'strandpile'
371
- ? strandpileLanes(rows)
372
- : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
373
- }
374
-
375
- /**
376
- * The spans a `features` panel draws, keyed by row name and measured in the
377
- * panel's own pixels. `column` takes the bands the overlay draws, at the
378
- * alignment's column width. `position` packs each row's features in its own
379
- * residue positions, shifted by an `align` transform, and maps the extent they
380
- * cover across every row onto the panel width, so a row with no alignment
381
- * still has an x.
382
- */
383
- function featurePanelSpans({
384
- panel,
385
- width,
386
- colWidth,
387
- domainBands,
388
- annotationsByRow,
389
- shifts,
390
- }: {
391
- panel: RowFeaturesSpec
392
- width: number
393
- colWidth: number
394
- domainBands: Map<string, DomainBand[]>
395
- annotationsByRow: Record<string, Annotation[]>
396
- shifts: Map<string, number> | undefined
397
- }): Map<string, RowPanelSpan[]> {
398
- if (panel.x === 'column') {
399
- return panelLanes(
400
- panel,
401
- [...domainBands].map(([name, bands]) => [
402
- name,
403
- bands.map(band => ({
404
- ...band,
405
- xStart: band.startCol * colWidth,
406
- xEnd: band.endCol * colWidth,
407
- })),
408
- ]),
409
- )
410
- }
411
- const shifted = Object.entries(annotationsByRow).map(
412
- ([name, annotations]) => {
413
- const shift = shifts?.get(name) ?? 0
414
- return [
415
- name,
416
- annotations.map(annotation => ({
417
- annotation,
418
- start: annotation.start - 1 + shift,
419
- end: annotation.end + shift,
420
- })),
421
- ] as const
422
- },
423
- )
424
- let min = Infinity
425
- let max = -Infinity
426
- for (const [, features] of shifted) {
427
- for (const { start, end } of features) {
428
- min = Math.min(min, start)
429
- max = Math.max(max, end)
430
- }
431
- }
432
- // a gene arrow ends where its feature does, so the extent maps onto the whole
433
- // panel less the pixel the rightmost stroke needs
434
- const drawable = Math.max(1, width - 1)
435
- const scale = max > min ? drawable / (max - min) : 0
436
- // a lane opens where a span covers more than a tenth of the one before it,
437
- // which keeps the genes of an operon on one lane: adjacent genes commonly
438
- // share a few bases, and a stop codon overlapping the next start reads as a
439
- // second lane over the whole row
440
- return panelLanes(
441
- panel,
442
- shifted.map(([name, features]) => [
443
- name,
444
- features.map(({ annotation, start, end }) => ({
445
- annotation,
446
- xStart: (start - min) * scale,
447
- xEnd: (end - min) * scale,
448
- startCol: start,
449
- endCol: end - (end - start) * laneOverlap,
450
- })),
451
- ]),
452
- )
453
- }
454
-
455
- // a scale over the values a field takes across every feature loaded, so hiding
456
- // a type in the filter dialog leaves the rest in their colors, and a legend of
457
- // the values the features drawn carry
458
- function resolveFeatureScale(
459
- field: string,
460
- scale: ScaleSpec | undefined,
461
- annotations: Annotation[],
462
- drawn: Annotation[],
463
- ) {
464
- const valuesOf = (list: Annotation[]) =>
465
- list.map(a => featureField(a, field)).filter(notEmpty)
466
- const resolved = resolveScale(scale, valuesOf(annotations))
467
- const shown = new Set(valuesOf(drawn))
468
- return {
469
- field,
470
- ...resolved,
471
- legend: resolved.legend.filter(entry => shown.has(entry.id)),
472
- }
473
- }
474
-
475
208
  const columnTrackBytes = new WeakMap<ColumnTrackSpec, number>()
476
209
 
477
210
  function columnTrackSize(track: ColumnTrackSpec) {
@@ -531,6 +264,7 @@ export const preservedOnReset = new Set([
531
264
  'drawNodeBubbles',
532
265
  'drawNodeLabels',
533
266
  'showTreeOverview',
267
+ 'showTreeScaleBar',
534
268
  'overviewHeight',
535
269
  'turnedOffTracks',
536
270
  'trackHeights',
@@ -538,224 +272,23 @@ export const preservedOnReset = new Set([
538
272
  'allowedGappyness',
539
273
  'subFeatureRows',
540
274
  'showDomainLegend',
275
+ 'domainFill',
276
+ 'embeddedMode',
541
277
  ])
542
278
 
543
279
  // `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
544
- // track writes nothing into the snapshot. A file-supplied track passes its own
545
- // `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
546
- // lines, an Rfam family one per row.
280
+ // track writes nothing into the snapshot
547
281
  function trackIsOff(
548
282
  turnedOffTracks: { get: (id: string) => boolean | undefined },
549
283
  id: string,
550
- defaultOff?: boolean,
551
284
  ) {
552
- return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
285
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
553
286
  }
554
287
 
555
- // the marks that set tree state from a node rather than drawing
556
- const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
557
-
558
288
  // shared empty results, so observers don't see a fresh [] as a change
559
289
  const noDomains: Annotation[] = []
560
290
  const noClades: ResolvedClade[] = []
561
291
 
562
- /**
563
- * The rows a clade covers and the node an `mrca` names, or undefined when the
564
- * clade does not resolve: a tip name the tree does not have or has twice, or a
565
- * leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
566
- * passes over the tree.
567
- */
568
- function cladeRows(
569
- clade: Clade,
570
- root: HierarchyNode<NodeWithIds>,
571
- index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
572
- rowNamesSet: Map<string, number>,
573
- ): { rows: [number, number]; nodeId?: string } | undefined {
574
- if (clade.range) {
575
- const [a, b] = clade.range.map(name =>
576
- index.get(name) ? rowNamesSet.get(name) : undefined,
577
- )
578
- if (a === undefined || b === undefined) {
579
- return undefined
580
- }
581
- const rows: [number, number] = a <= b ? [a, b] : [b, a]
582
- return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined
583
- }
584
- const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
585
- if (!node) {
586
- return undefined
587
- }
588
- const tips = leaves(node)
589
- if (tips.length !== clade.tips) {
590
- return undefined
591
- }
592
- let first = Infinity
593
- let last = -Infinity
594
- for (const tip of tips) {
595
- const row = rowNamesSet.get(tip.data.name)
596
- if (row !== undefined) {
597
- first = Math.min(first, row)
598
- last = Math.max(last, row)
599
- }
600
- }
601
- return first <= last
602
- ? { rows: [first, last], nodeId: node.data.id }
603
- : undefined
604
- }
605
-
606
- /**
607
- * Every clade that resolves, with its fill color settled, against the leaf
608
- * order `rowNamesSet` gives. The tree panel resolves against the displayed
609
- * rows and the overview against its own, so both take the tree they draw. A
610
- * `range` record names no node, so the seeding marks, which need one, drop it.
611
- */
612
- function resolveClades(
613
- clades: Clade[],
614
- root: HierarchyNode<NodeWithIds>,
615
- rowNamesSet: Map<string, number>,
616
- ): ResolvedClade[] {
617
- const index = leafIndex(root)
618
- return clades.flatMap(clade => {
619
- const resolved = cladeRows(clade, root, index, rowNamesSet)
620
- const seeding = seedingMarks.has(clade.mark)
621
- return resolved && !(seeding && resolved.nodeId === undefined)
622
- ? [
623
- {
624
- ...resolved,
625
- mark: clade.mark,
626
- color: withAlpha(
627
- clade.color ?? cladeHighlightColor,
628
- cladeHighlightAlpha,
629
- ),
630
- markColor: clade.color,
631
- label: clade.label,
632
- },
633
- ]
634
- : []
635
- })
636
- }
637
-
638
- const childOrders: Record<
639
- TreeOrder,
640
- ((a: HierarchyNode, b: HierarchyNode) => number) | undefined
641
- > = {
642
- input: undefined,
643
- branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
644
- ladderize: (a, b) => a.value! - b.value!,
645
- ladderizeReverse: (a, b) => b.value! - a.value!,
646
- }
647
-
648
- /**
649
- * The tree with the display transforms applied: leaf counts summed, children
650
- * put in `order` and each `rotated` node's reversed, `showOnly` taken as the
651
- * new root, and each `collapsed` clade folded. The overview builds the same
652
- * tree without the focus, so it shows the focused subtree inside the whole.
653
- */
654
- function buildTreeRoot(
655
- tree: NodeWithIds,
656
- {
657
- collapsed,
658
- rotated,
659
- order,
660
- showOnly,
661
- }: {
662
- collapsed: readonly string[]
663
- rotated: readonly string[]
664
- order: TreeOrder
665
- showOnly?: string
666
- },
667
- ) {
668
- let hier = hierarchy(tree, d => d.children)
669
- hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
670
- const compare = childOrders[order]
671
- if (compare) {
672
- sort(hier, compare)
673
- }
674
- if (rotated.length > 0) {
675
- const flip = new Set(rotated)
676
- forEachDescendant(hier, n => {
677
- if (flip.has(n.data.id)) {
678
- n.children?.reverse()
679
- }
680
- })
681
- }
682
-
683
- if (showOnly) {
684
- const res = find(hier, n => n.data.id === showOnly)
685
- if (res) {
686
- hier = res
687
- }
688
- }
689
-
690
- for (const collapsedId of collapsed) {
691
- const node = find(hier, n => n.data.id === collapsedId)
692
- if (node) {
693
- if (node.children) {
694
- collapse(node)
695
- } else if (node.parent?.children) {
696
- node.parent.children = node.parent.children.filter(
697
- c => c.data.id !== collapsedId,
698
- )
699
- }
700
- }
701
- }
702
-
703
- return hier
704
- }
705
-
706
- /**
707
- * The node whose branch an outgroup roots the tree on: the outgroup's most
708
- * recent common ancestor, or the rest of the tips' where that ancestor is the
709
- * root, since the outgroup then straddles the root and the branch above the
710
- * other side is the same branch of the unrooted tree.
711
- */
712
- function outgroupNode(root: HierarchyNode, outgroup: string[]) {
713
- const index = leafIndex(root)
714
- const node = mrca(root, outgroup, index)
715
- if (node !== root) {
716
- return node
717
- }
718
- const out = new Set(outgroup)
719
- const rest = leaves(root)
720
- .map(n => n.data.name)
721
- .filter(name => !out.has(name))
722
- const other = mrca(root, rest, index)
723
- return other === root ? undefined : other
724
- }
725
-
726
- /**
727
- * `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
728
- * as written become names on unnamed nodes, and a rerooting would carry them
729
- * along as labels, so they come off first.
730
- */
731
- function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
732
- const plain = new Map<NodeWithIds, TreeNode>()
733
- for (const node of preorder(tree).reverse()) {
734
- plain.set(node, {
735
- name: node.name === node.id ? undefined : node.name,
736
- length: node.length,
737
- children: node.children.map(c => plain.get(c)!),
738
- })
739
- }
740
- const input = plain.get(tree)!
741
- if (treeRoot === 'midpoint') {
742
- return generateNodeIds(midpointRoot(input))
743
- }
744
- const target = outgroupNode(
745
- hierarchy(tree, d => d.children),
746
- treeRoot.outgroup,
747
- )
748
- return target
749
- ? generateNodeIds(rerootTree(input, plain.get(target.data)!))
750
- : tree
751
- }
752
-
753
- // the inclusive tip indices a node covers, from the row-space extent
754
- // clusterLayout writes to xMin/xMax as tip centers
755
- function tipRange(node: HierarchyNode): [number, number] {
756
- return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
757
- }
758
-
759
292
  // the channels reading the feature table; every other channel reads rowData
760
293
  const featureChannels = new Set<EncodingChannel>([
761
294
  'featureFill',
@@ -765,6 +298,16 @@ const featureChannels = new Set<EncodingChannel>([
765
298
  // seqPos -> column indexes per row, keyed on the parse so they are garbage
766
299
  // collected with it. A computed would rebuild every row's index when read
767
300
  // outside a reactive context.
301
+ // Newick tools write a colon in a name as an underscore, so a tree's row
302
+ // `chr1_100-200` is the GFF's `chr1:100-200`, as FastaMSA.getRow resolves it
303
+ function annotationRowName(id: string, rowNames: Map<string, number>) {
304
+ if (rowNames.has(id)) {
305
+ return id
306
+ }
307
+ const underscored = id.replaceAll(':', '_')
308
+ return rowNames.has(underscored) ? underscored : id
309
+ }
310
+
768
311
  const seqPosIndexCache = new WeakMap<object, Map<string, Int32Array>>()
769
312
 
770
313
  // A segment is a 1:1 run. A segment whose sides differ in length is malformed,
@@ -776,6 +319,31 @@ function sameLength(segment: ResidueSegment) {
776
319
  )
777
320
  }
778
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
+
779
347
  // fit() divides the viewport by the row or column count and multiplies back, so
780
348
  // an exact fit can land a fraction of a pixel over. A plain `>` would then show
781
349
  // a scrollbar that shrinks the viewport and leaves a gap.
@@ -810,19 +378,11 @@ function sharedValue(values: (string | undefined)[]) {
810
378
  : undefined
811
379
  }
812
380
 
813
- // preorder, so reversing the list puts every node after its descendants
814
- function preorder(tree: NodeWithIds) {
815
- const order: NodeWithIds[] = []
816
- const stack = [tree]
817
- while (stack.length > 0) {
818
- const node = stack.pop()!
819
- order.push(node)
820
- for (const child of node.children) {
821
- stack.push(child)
822
- }
823
- }
824
- return order
825
- }
381
+ type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
382
+ type FeaturePanelScale = Omit<
383
+ ResolvedFeaturePanel,
384
+ 'id' | 'width' | 'offsetX' | 'spans'
385
+ >
826
386
 
827
387
  /**
828
388
  * #stateModel MsaView
@@ -868,6 +428,13 @@ function stateModelFactory({
868
428
  * #property
869
429
  */
870
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),
871
438
  /**
872
439
  * #property
873
440
  * whether the domain legend is expanded. The legend floats over the
@@ -899,6 +466,13 @@ function stateModelFactory({
899
466
  */
900
467
  drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
901
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
+
902
476
  /**
903
477
  * #property
904
478
  * zoom in/out on plain mouse-wheel without holding ctrl
@@ -1051,10 +625,10 @@ function stateModelFactory({
1051
625
 
1052
626
  /**
1053
627
  * #property
1054
- * the user's explicit hide choices per annotation accession, keyed by
1055
- * accession with the value meaning "off", like `turnedOffTracks`. An
1056
- * untouched accession is absent and drawn, so the shared URL grows
1057
- * 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
1058
632
  */
1059
633
  turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
1060
634
  /**
@@ -1096,9 +670,10 @@ function stateModelFactory({
1096
670
  /**
1097
671
  * #property
1098
672
  * where the view opens, in `highlights` coordinates: `{row, start,
1099
- * end}` zooms onto residues of that row, `{start, end}` onto alignment
1100
- * columns. Applied once the alignment and any tree file have loaded,
1101
- * then cleared, so a reloaded session keeps the reader's own scroll.
673
+ * end}` zooms onto residues of that row and scrolls the row to the
674
+ * middle, `{start, end}` zooms onto alignment columns. Applied once the
675
+ * alignment and any tree file have loaded, then cleared, so a reloaded
676
+ * session keeps the reader's own scroll.
1102
677
  */
1103
678
  region: types.frozen<Region | undefined>(),
1104
679
  /**
@@ -1134,12 +709,30 @@ function stateModelFactory({
1134
709
  * #volatile
1135
710
  */
1136
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 },
1137
724
  /**
1138
725
  * #volatile
1139
726
  * leaves the toolbar out, for a host drawing its own controls. Kept out
1140
727
  * of the snapshot so a link opened in the full app shows the toolbar.
1141
728
  */
1142
729
  hideHeader: false,
730
+ /**
731
+ * #volatile
732
+ * shows the import form while the view has no data. MSAViewer turns it
733
+ * off, since its host supplies the data and the form is the demo app's
734
+ */
735
+ importForm: true,
1143
736
  /**
1144
737
  * #volatile
1145
738
  */
@@ -1151,6 +744,13 @@ function stateModelFactory({
1151
744
  highResScaleFactor:
1152
745
  typeof window === 'undefined' ? 1 : window.devicePixelRatio,
1153
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,
1154
754
  /**
1155
755
  * #volatile
1156
756
  */
@@ -1313,6 +913,12 @@ function stateModelFactory({
1313
913
  setWidth(arg: number) {
1314
914
  self.volatileWidth = arg
1315
915
  },
916
+ /**
917
+ * #action
918
+ */
919
+ setSettingsOpen(arg: boolean) {
920
+ self.settingsOpen = arg
921
+ },
1316
922
  /**
1317
923
  * #action
1318
924
  * update the canvas scale factor when the device pixel ratio changes
@@ -1456,6 +1062,23 @@ function stateModelFactory({
1456
1062
  setShowDomains(arg: boolean) {
1457
1063
  self.showDomains = arg
1458
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
+ },
1459
1082
  /**
1460
1083
  * #action
1461
1084
  * expand or collapse the domain legend that floats over the alignment
@@ -1484,6 +1107,7 @@ function stateModelFactory({
1484
1107
  */
1485
1108
  setRowHeight(n: number) {
1486
1109
  self.rowHeight = n
1110
+ self.zoomHeld = { ...self.zoomHeld, row: 0 }
1487
1111
  },
1488
1112
 
1489
1113
  /**
@@ -1492,6 +1116,7 @@ function stateModelFactory({
1492
1116
  */
1493
1117
  setColWidth(n: number) {
1494
1118
  self.colWidth = n
1119
+ self.zoomHeld = { ...self.zoomHeld, col: 0 }
1495
1120
  },
1496
1121
 
1497
1122
  /**
@@ -1651,36 +1276,19 @@ function stateModelFactory({
1651
1276
  .views(self => ({
1652
1277
  /**
1653
1278
  * #getter
1654
- * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
1279
+ * hideGaps takes effect when a collapse or a focus hides rows, whose
1280
+ * residues leave columns all gaps among the rest, or when
1281
+ * allowedGappyness is under 100
1655
1282
  */
1656
1283
  get hideGapsEffective() {
1657
1284
  return (
1658
1285
  self.hideGaps &&
1659
- (self.collapsed.length > 0 || self.allowedGappyness < 100)
1286
+ (self.collapsed.length > 0 ||
1287
+ self.showOnly !== undefined ||
1288
+ self.allowedGappyness < 100)
1660
1289
  )
1661
1290
  },
1662
- /**
1663
- * #getter
1664
- */
1665
- get realAllowedGappyness() {
1666
- return this.hideGapsEffective ? self.allowedGappyness : 100
1667
- },
1668
- /**
1669
- * #getter
1670
- * the annotations the overlay, the legend and the encodings draw: the
1671
- * GFF's, then the `features` layer's
1672
- */
1673
- get allAnnotations(): Annotation[] {
1674
- return self.features.length > 0
1675
- ? [...self.annotations, ...featuresToAnnotations(self.features)]
1676
- : self.annotations
1677
- },
1678
- /**
1679
- * #getter
1680
- */
1681
- get actuallyShowDomains() {
1682
- return self.showDomains && this.allAnnotations.length > 0
1683
- },
1291
+
1684
1292
  /**
1685
1293
  * #getter
1686
1294
  * whether the host restores the loaded documents outside the snapshot.
@@ -1705,7 +1313,7 @@ function stateModelFactory({
1705
1313
  if (self.volatileWidth === undefined) {
1706
1314
  throw new Error('not initialized')
1707
1315
  }
1708
- return self.volatileWidth
1316
+ return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0)
1709
1317
  },
1710
1318
  }))
1711
1319
  .views(self => ({
@@ -1755,7 +1363,7 @@ function stateModelFactory({
1755
1363
  return !!this.tree.noTree
1756
1364
  },
1757
1365
  get noDomains() {
1758
- return self.allAnnotations.length === 0
1366
+ return this.allAnnotations.length === 0
1759
1367
  },
1760
1368
 
1761
1369
  /**
@@ -1864,6 +1472,27 @@ function stateModelFactory({
1864
1472
  ? parseMSA(text, self.currentAlignment, self.msaFormat)
1865
1473
  : null
1866
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
+ },
1867
1496
  /**
1868
1497
  * #getter
1869
1498
  */
@@ -1931,7 +1560,7 @@ function stateModelFactory({
1931
1560
  return resolveClades(
1932
1561
  self.clades,
1933
1562
  hierarchy(this.tree, d => d.children),
1934
- this.rowNamesSet,
1563
+ this.root,
1935
1564
  )
1936
1565
  },
1937
1566
  /**
@@ -2120,21 +1749,19 @@ function stateModelFactory({
2120
1749
  if (!self.showTreeOverview) {
2121
1750
  return undefined
2122
1751
  }
2123
- const root = buildTreeRoot(this.tree, {
2124
- collapsed: self.collapsed,
2125
- rotated: self.rotated,
2126
- order: self.treeOrder,
2127
- })
2128
- const numTips = leaves(root).length
2129
- clusterLayout(root, numTips, 1)
2130
- const rootLen = Math.max(root.data.length || 0, 0)
2131
- const extent = maxLength(root) - rootLen
2132
- setBrLength(root, -rootLen, extent ? 1 / extent : 0)
1752
+ const layout = layoutTree(
1753
+ buildTreeRoot(this.tree, {
1754
+ collapsed: self.collapsed,
1755
+ rotated: self.rotated,
1756
+ order: self.treeOrder,
1757
+ }),
1758
+ { rowHeight: 1, treeWidth: 1 },
1759
+ )
2133
1760
  return {
2134
- root,
2135
- numTips,
2136
- maxDepthToLeaf: calcDepthToLeaf(root),
2137
- showBranchLen: self.showBranchLen && extent > 0,
1761
+ root: layout.root,
1762
+ numTips: layout.leaves.length,
1763
+ maxDepthToLeaf: layout.root.depthToLeaf,
1764
+ showBranchLen: self.showBranchLen && layout.rootToTipLength > 0,
2138
1765
  }
2139
1766
  },
2140
1767
 
@@ -2148,10 +1775,11 @@ function stateModelFactory({
2148
1775
  if (!layout || self.clades.length === 0) {
2149
1776
  return noClades
2150
1777
  }
2151
- const rowNames = new Map(
2152
- leaves(layout.root).map((leaf, index) => [leaf.data.name, index]),
1778
+ return resolveClades(
1779
+ self.clades,
1780
+ hierarchy(this.tree, d => d.children),
1781
+ layout.root,
2153
1782
  )
2154
- return resolveClades(self.clades, layout.root, rowNames)
2155
1783
  },
2156
1784
 
2157
1785
  /**
@@ -2244,7 +1872,7 @@ function stateModelFactory({
2244
1872
  * #getter
2245
1873
  */
2246
1874
  get blanks() {
2247
- const { hideGapsEffective, realAllowedGappyness } = self
1875
+ const { hideGapsEffective, allowedGappyness } = self
2248
1876
  if (!hideGapsEffective) {
2249
1877
  return []
2250
1878
  }
@@ -2260,7 +1888,7 @@ function stateModelFactory({
2260
1888
  // row that stops early counts as gapped for the remainder
2261
1889
  const numCols = this.MSA?.getWidth() ?? 0
2262
1890
  const numRows = strs.length
2263
- const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
1891
+ const threshold = Math.ceil((allowedGappyness / 100) * numRows)
2264
1892
  const blankCounts = new Uint32Array(numCols)
2265
1893
  for (let j = 0; j < numRows; j++) {
2266
1894
  const str = strs[j]!
@@ -2344,11 +1972,19 @@ function stateModelFactory({
2344
1972
  return index
2345
1973
  },
2346
1974
 
1975
+ /**
1976
+ * #method
1977
+ * the row's aligned sequence by the name the tree gives it, including
1978
+ * rows a collapsed clade hides. `MSA.getRow` resolves a FASTA name whose
1979
+ * colon a Newick tool wrote as an underscore, which `rowMap` does not
1980
+ */
1981
+ rowSequence(name: string) {
1982
+ return this.MSA?.getRow(name) || undefined
1983
+ },
2347
1984
  /**
2348
1985
  * #getter
2349
- * every sequence in the alignment, keyed by row name, including rows a
2350
- * collapsed clade hides. `rows` holds only the rows on screen; lookups
2351
- * by row name use this
1986
+ * every sequence in the alignment, keyed by the alignment's own row
1987
+ * names, including rows a collapsed clade hides
2352
1988
  */
2353
1989
  get rowMap() {
2354
1990
  const MSA = this.MSA
@@ -2399,10 +2035,11 @@ function stateModelFactory({
2399
2035
  */
2400
2036
  get sequenceType(): 'dna' | 'rna' | 'amino' {
2401
2037
  const letters = this.colStats.lettersPresent
2402
- // isDna already excludes U (not in the DNA set) and isRna excludes T,
2403
- // so the set membership alone disambiguates the two
2404
- const dna = new Set(['A', 'C', 'G', 'T', 'N'])
2405
- const rna = new Set(['A', 'C', 'G', 'U', 'N'])
2038
+ // the two-base IUPAC codes keep an ambiguous read nucleotide; the
2039
+ // three-base ones (B, D, H, V) are common amino acids, so they don't
2040
+ const ambiguous = ['N', 'R', 'Y', 'K', 'M', 'S', 'W']
2041
+ const dna = new Set(['A', 'C', 'G', 'T', ...ambiguous])
2042
+ const rna = new Set(['A', 'C', 'G', 'U', ...ambiguous])
2406
2043
  const isDna = letters.size > 0 && [...letters].every(l => dna.has(l))
2407
2044
  const isRna = letters.size > 0 && [...letters].every(l => rna.has(l))
2408
2045
  return isDna ? 'dna' : isRna ? 'rna' : 'amino'
@@ -2449,6 +2086,22 @@ function stateModelFactory({
2449
2086
  )
2450
2087
  },
2451
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
+
2452
2105
  /**
2453
2106
  * #getter
2454
2107
  * Conservation score per column using Shannon entropy (biojs-msa style).
@@ -2536,15 +2189,6 @@ function stateModelFactory({
2536
2189
  return this.treeLayout.rootToTipLength
2537
2190
  },
2538
2191
 
2539
- /**
2540
- * #getter
2541
- * x-position of the farthest tip in a phylogram, px: effectiveTreeWidth,
2542
- * or 0 for a tree with no branch lengths (drawn as a cladogram)
2543
- */
2544
- get maxBranchLength() {
2545
- return this.rootToTipLength ? this.effectiveTreeWidth : 0
2546
- },
2547
-
2548
2192
  /**
2549
2193
  * #getter
2550
2194
  * pixels per unit of branch length in the phylogram layout, 0 in
@@ -2721,7 +2365,6 @@ function stateModelFactory({
2721
2365
  data: content?.data,
2722
2366
  arcs: content?.arcs,
2723
2367
  },
2724
- ReactComponent: TrackBlocks,
2725
2368
  }
2726
2369
  })
2727
2370
  columnTrackModels.set(track, computedModel)
@@ -2783,7 +2426,6 @@ function stateModelFactory({
2783
2426
  data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
2784
2427
  height: rowHeight,
2785
2428
  },
2786
- ReactComponent: TrackBlocks,
2787
2429
  }))
2788
2430
  },
2789
2431
 
@@ -2806,7 +2448,6 @@ function stateModelFactory({
2806
2448
  height: self.trackHeight('arc'),
2807
2449
  arcs,
2808
2450
  },
2809
- ReactComponent: TrackBlocks,
2810
2451
  },
2811
2452
  ]
2812
2453
  : []
@@ -2854,7 +2495,6 @@ function stateModelFactory({
2854
2495
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2855
2496
  height: self.trackHeight(model.kind),
2856
2497
  },
2857
- ReactComponent: TrackBlocks,
2858
2498
  }))
2859
2499
  },
2860
2500
 
@@ -2874,8 +2514,7 @@ function stateModelFactory({
2874
2514
  */
2875
2515
  get turnedOnTracks() {
2876
2516
  return this.tracks.filter(
2877
- f =>
2878
- !trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff),
2517
+ f => !trackIsOff(self.turnedOffTracks, f.model.id),
2879
2518
  )
2880
2519
  },
2881
2520
 
@@ -2897,7 +2536,7 @@ function stateModelFactory({
2897
2536
  * @returns The character at that position
2898
2537
  */
2899
2538
  visibleColToRowLetter(rowName: string, visibleCol: number) {
2900
- return self.rowMap.get(rowName)?.[
2539
+ return self.rowSequence(rowName)?.[
2901
2540
  this.visibleColToGlobalCol(visibleCol)
2902
2541
  ]
2903
2542
  },
@@ -2982,7 +2621,7 @@ function stateModelFactory({
2982
2621
  * @returns The global column index in the full MSA, or undefined
2983
2622
  */
2984
2623
  seqPosToGlobalCol(rowName: string, seqPos: number) {
2985
- const seq = self.rowMap.get(rowName)
2624
+ const seq = self.rowSequence(rowName)
2986
2625
  if (seq === undefined) {
2987
2626
  return undefined
2988
2627
  }
@@ -3205,7 +2844,11 @@ function stateModelFactory({
3205
2844
  if (mapping.structure.id !== structureId) {
3206
2845
  continue
3207
2846
  }
3208
- if (asymId !== undefined && mapping.structure.asymId !== asymId) {
2847
+ if (
2848
+ asymId !== undefined &&
2849
+ mapping.structure.asymId !== undefined &&
2850
+ mapping.structure.asymId !== asymId
2851
+ ) {
3209
2852
  continue
3210
2853
  }
3211
2854
  const segment = mapping.segments.find(
@@ -3225,10 +2868,14 @@ function stateModelFactory({
3225
2868
  .views(self => ({
3226
2869
  /**
3227
2870
  * #getter
3228
- * the branch-length scale bar over the tree, undefined in cladogram mode
3229
- * or when the tree area is too narrow for one
2871
+ * the branch-length scale bar over the tree, undefined while
2872
+ * `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
2873
+ * too narrow for one
3230
2874
  */
3231
2875
  get treeScaleBar() {
2876
+ if (!self.showTreeScaleBar) {
2877
+ return undefined
2878
+ }
3232
2879
  return scaleBarLength(
3233
2880
  self.pxPerBranchLength,
3234
2881
  self.effectiveTreeAreaWidth - self.marginLeft * 2,
@@ -3284,13 +2931,48 @@ function stateModelFactory({
3284
2931
  }
3285
2932
  return types
3286
2933
  },
3287
- get filteredAnnotations() {
3288
- return self.allAnnotations.filter(
3289
- r => !self.turnedOffFeatures.get(r.accession),
2934
+ /**
2935
+ * accessions hidden until the user turns them on, such as a Stockholm
2936
+ * file's #=GR lines
2937
+ */
2938
+ get defaultOffAccessions() {
2939
+ return new Set(
2940
+ self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession),
3290
2941
  )
3291
2942
  },
2943
+ /**
2944
+ * #method
2945
+ */
2946
+ featureOff(accession: string) {
2947
+ return (
2948
+ self.turnedOffFeatures.get(accession) ??
2949
+ this.defaultOffAccessions.has(accession)
2950
+ )
2951
+ },
2952
+ get filteredAnnotations() {
2953
+ return self.allAnnotations.filter(r => !this.featureOff(r.accession))
2954
+ },
2955
+ /**
2956
+ * #getter
2957
+ * what colors the alignment's cells: the residue scheme, the domains
2958
+ * covering them, or nothing, which leaves the scheme to the letters.
2959
+ * Domains own the fill only while some are drawn, so a file whose
2960
+ * annotations all start hidden keeps its residue colors
2961
+ */
2962
+ get cellFill(): 'residue' | 'domain' | 'none' {
2963
+ return !self.bgColor
2964
+ ? 'none'
2965
+ : self.actuallyShowDomains &&
2966
+ self.domainFill &&
2967
+ this.filteredAnnotations.length > 0
2968
+ ? 'domain'
2969
+ : 'residue'
2970
+ },
3292
2971
  get annotationsByRow() {
3293
- return groupBy(this.filteredAnnotations, r => r.id)
2972
+ const { rowNamesSet } = self
2973
+ return groupBy(this.filteredAnnotations, r =>
2974
+ annotationRowName(r.id, rowNamesSet),
2975
+ )
3294
2976
  },
3295
2977
  }))
3296
2978
  .views(self => ({
@@ -3364,6 +3046,12 @@ function stateModelFactory({
3364
3046
  self.blocksX.map(bx => [bx, by] as const),
3365
3047
  )
3366
3048
  },
3049
+ /**
3050
+ * #getter
3051
+ */
3052
+ get wheelZooming() {
3053
+ return self.zoomSnapshot !== undefined
3054
+ },
3367
3055
 
3368
3056
  /**
3369
3057
  * #getter
@@ -3381,7 +3069,9 @@ function stateModelFactory({
3381
3069
  const { annotations, rowNamesSet } = self
3382
3070
  return self.dataInitialized &&
3383
3071
  annotations.length > 0 &&
3384
- !annotations.some(a => rowNamesSet.has(a.id))
3072
+ !annotations.some(a =>
3073
+ rowNamesSet.has(annotationRowName(a.id, rowNamesSet)),
3074
+ )
3385
3075
  ? [
3386
3076
  `0 of ${annotations.length} annotations name a row in this alignment`,
3387
3077
  ]
@@ -3521,10 +3211,13 @@ function stateModelFactory({
3521
3211
  * #action
3522
3212
  * swap in a different tree over the same alignment. Clears `collapsed`,
3523
3213
  * `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
3524
- * tree would match unrelated nodes in the new one.
3214
+ * tree would match unrelated nodes in the new one, and the tree's
3215
+ * filehandle, which the snapshot would otherwise reopen in place of
3216
+ * the new tree
3525
3217
  */
3526
3218
  replaceTree(newick: string) {
3527
3219
  transaction(() => {
3220
+ self.setTreeFilehandle(undefined)
3528
3221
  self.collapsed.clear()
3529
3222
  self.rotated.clear()
3530
3223
  self.setShowOnly(undefined)
@@ -3552,30 +3245,6 @@ function stateModelFactory({
3552
3245
  axis,
3553
3246
  )
3554
3247
  },
3555
- /**
3556
- * #action
3557
- */
3558
- zoomOutHorizontal() {
3559
- this.zoomAtCenter(0.75, 'horizontal')
3560
- },
3561
- /**
3562
- * #action
3563
- */
3564
- zoomInHorizontal() {
3565
- this.zoomAtCenter(1.5, 'horizontal')
3566
- },
3567
- /**
3568
- * #action
3569
- */
3570
- zoomInVertical() {
3571
- this.zoomAtCenter(1.5, 'vertical')
3572
- },
3573
- /**
3574
- * #action
3575
- */
3576
- zoomOutVertical() {
3577
- this.zoomAtCenter(0.75, 'vertical')
3578
- },
3579
3248
  /**
3580
3249
  * #action
3581
3250
  */
@@ -3608,19 +3277,35 @@ function stateModelFactory({
3608
3277
  transaction(() => {
3609
3278
  const colInView = (-self.scrollX + offsetX) / self.colWidth
3610
3279
  const rowInView = (-self.scrollY + offsetY) / self.rowHeight
3280
+ // zooming both axes, an axis holds at the default only while the
3281
+ // other sits there too, or one would hold while the other grows
3282
+ const canHold =
3283
+ axis !== 'both' ||
3284
+ (self.colWidth === defaultColWidth &&
3285
+ self.rowHeight === defaultRowHeight)
3611
3286
  if (axis !== 'vertical') {
3612
- self.colWidth = clamp(
3613
- self.colWidth * scaleFactor,
3287
+ const { value, held } = detentZoom(
3288
+ self.colWidth,
3289
+ scaleFactor,
3290
+ defaultColWidth,
3291
+ self.zoomHeld.col,
3614
3292
  minColWidth,
3615
- maxCellSize,
3293
+ canHold,
3616
3294
  )
3295
+ self.colWidth = value
3296
+ self.zoomHeld = { ...self.zoomHeld, col: held }
3617
3297
  }
3618
3298
  if (axis !== 'horizontal') {
3619
- self.rowHeight = clamp(
3620
- self.rowHeight * scaleFactor,
3299
+ const { value, held } = detentZoom(
3300
+ self.rowHeight,
3301
+ scaleFactor,
3302
+ defaultRowHeight,
3303
+ self.zoomHeld.row,
3621
3304
  minRowHeight,
3622
- maxCellSize,
3305
+ canHold,
3623
3306
  )
3307
+ self.rowHeight = value
3308
+ self.zoomHeld = { ...self.zoomHeld, row: held }
3624
3309
  }
3625
3310
  self.scrollX = clamp(
3626
3311
  offsetX - colInView * self.colWidth,
@@ -3641,6 +3326,25 @@ function stateModelFactory({
3641
3326
  )
3642
3327
  })
3643
3328
  },
3329
+ /**
3330
+ * #action
3331
+ * zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
3332
+ * it drew at the start of the gesture, scaled, and redraws them only once
3333
+ * they stop covering the viewport; endWheelZoom redraws them sharp
3334
+ */
3335
+ wheelZoomToPos(scaleFactor: number, offsetX: number, offsetY: number) {
3336
+ self.zoomSnapshot ??= takeZoomSnapshot(self, false)
3337
+ this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis)
3338
+ if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
3339
+ self.zoomSnapshot = takeZoomSnapshot(self, true)
3340
+ }
3341
+ },
3342
+ /**
3343
+ * #action
3344
+ */
3345
+ endWheelZoom() {
3346
+ self.zoomSnapshot = undefined
3347
+ },
3644
3348
  /**
3645
3349
  * #action
3646
3350
  */
@@ -3668,16 +3372,6 @@ function stateModelFactory({
3668
3372
  self.annotations = annotations
3669
3373
  },
3670
3374
 
3671
- /**
3672
- * #action
3673
- * set the overlay from raw InterProScan results keyed by row name. Kept
3674
- * for downstream plugins that hold the EBI wire format; new code should
3675
- * adapt to Annotation[] and call setAnnotations.
3676
- */
3677
- setDomains(data?: Record<string, InterProScanResults>) {
3678
- this.setAnnotations(data ? interProScanToAnnotations(data) : [])
3679
- },
3680
-
3681
3375
  applyGFFText(gffText: string) {
3682
3376
  this.setAnnotations(gffToAnnotations(parseGFF(gffText)))
3683
3377
  },
@@ -3735,11 +3429,22 @@ function stateModelFactory({
3735
3429
  * #action
3736
3430
  */
3737
3431
  toggleTrack(id: string) {
3738
- const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff
3739
- self.turnedOffTracks.set(
3740
- id,
3741
- !trackIsOff(self.turnedOffTracks, id, defaultOff),
3742
- )
3432
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
3433
+ },
3434
+ /**
3435
+ * #action
3436
+ * show or hide several tracks at once, as the track selector's Show all
3437
+ * and Hide all do. A choice matching the track's default is dropped, so
3438
+ * it stays out of the shared URL
3439
+ */
3440
+ setTracksShown(ids: string[], shown: boolean) {
3441
+ for (const id of ids) {
3442
+ if (defaultOffTracks.has(id) === !shown) {
3443
+ self.turnedOffTracks.delete(id)
3444
+ } else {
3445
+ self.turnedOffTracks.set(id, !shown)
3446
+ }
3447
+ }
3743
3448
  },
3744
3449
  /**
3745
3450
  * #action
@@ -3765,7 +3470,7 @@ function stateModelFactory({
3765
3470
  column: column + 1,
3766
3471
  row,
3767
3472
  residue: self.visibleColToSeqPosOneBased(row, visibleCol),
3768
- letter: self.rowMap.get(row)?.[column],
3473
+ letter: self.rowSequence(row)?.[column],
3769
3474
  }
3770
3475
  },
3771
3476
  /**
@@ -3904,7 +3609,7 @@ function stateModelFactory({
3904
3609
  */
3905
3610
  get visibleDomainTypes() {
3906
3611
  return this.categoricalDomainTypes
3907
- .filter(d => !self.turnedOffFeatures.get(d.accession))
3612
+ .filter(d => !self.featureOff(d.accession))
3908
3613
  .toSorted((a, b) => a.start - b.start)
3909
3614
  },
3910
3615
 
@@ -3954,7 +3659,7 @@ function stateModelFactory({
3954
3659
  : visibleDomainTypes.map(d => ({
3955
3660
  id: d.accession,
3956
3661
  label: d.name,
3957
- color: fillPalette[d.accession]!,
3662
+ color: d.color ?? fillPalette[d.accession]!,
3958
3663
  })),
3959
3664
  'domains',
3960
3665
  )
@@ -3973,16 +3678,16 @@ function stateModelFactory({
3973
3678
  /**
3974
3679
  * #getter
3975
3680
  * whether the overlay marks each domain with a bar under its row instead
3976
- * of filling the row behind the letters. Letter-color mode hands the
3977
- * background to the color scheme, so a filled box would paint over it and
3978
- * leave the setting with nothing to show. Sub-row layout already stacks
3681
+ * of filling the row behind the letters: whenever the domains do not own
3682
+ * the cell fill (`cellFill`), a filled box would paint over the residue
3683
+ * colors the user picked. Sub-row layout already stacks
3979
3684
  * the boxes clear of the letters, and with the letters too small to draw
3980
3685
  * the filled box is the only thing left to read.
3981
3686
  */
3982
3687
  get domainUnderline() {
3983
3688
  return (
3984
3689
  self.actuallyShowDomains &&
3985
- !self.bgColor &&
3690
+ self.cellFill !== 'domain' &&
3986
3691
  !self.subFeatureRows &&
3987
3692
  self.showMsaLetters
3988
3693
  )
@@ -4221,7 +3926,7 @@ function stateModelFactory({
4221
3926
  * to `end`, gaps included
4222
3927
  */
4223
3928
  get selectionFasta() {
4224
- const { rowNames, rowMap } = self
3929
+ const { rowNames } = self
4225
3930
  const resolved = this.resolvedSelection
4226
3931
  const span = this.selectionFileSpan
4227
3932
  if (!resolved || !span) {
@@ -4230,7 +3935,7 @@ function stateModelFactory({
4230
3935
  const rows = resolved.rowRuns
4231
3936
  .flatMap(([first, last]) => rowNames.slice(first, last + 1))
4232
3937
  .flatMap(name => {
4233
- const seq = rowMap.get(name)
3938
+ const seq = self.rowSequence(name)
4234
3939
  return seq === undefined ? [] : [[name, seq] as const]
4235
3940
  })
4236
3941
  return blockFasta(rows, span.start, span.end)
@@ -4327,7 +4032,7 @@ function stateModelFactory({
4327
4032
  this.visibleDomainTypes.map(d => ({
4328
4033
  id: d.accession,
4329
4034
  label: d.name,
4330
- color: fillPalette[d.accession]!,
4035
+ color: d.color ?? fillPalette[d.accession]!,
4331
4036
  })),
4332
4037
  }
4333
4038
  }
@@ -4504,6 +4209,18 @@ function stateModelFactory({
4504
4209
  setHideHeader(arg: boolean) {
4505
4210
  self.hideHeader = arg
4506
4211
  },
4212
+ /**
4213
+ * #action
4214
+ */
4215
+ setEmbeddedMode(arg: boolean) {
4216
+ self.embeddedMode = arg
4217
+ },
4218
+ /**
4219
+ * #action
4220
+ */
4221
+ setImportForm(arg: boolean) {
4222
+ self.importForm = arg
4223
+ },
4507
4224
  /**
4508
4225
  * #action
4509
4226
  * focus the subtree a click `y` pixels down the tree overview lands on.
@@ -4577,14 +4294,15 @@ function stateModelFactory({
4577
4294
  },
4578
4295
  /**
4579
4296
  * #action
4580
- * show or hide an annotation type. Only hidden types are recorded; see
4581
- * `turnedOffFeatures`
4297
+ * show or hide an annotation type. Only a choice that differs from the
4298
+ * type's default is recorded; see `turnedOffFeatures`
4582
4299
  */
4583
4300
  setFilter(accession: string, shown: boolean) {
4584
- if (shown) {
4585
- self.turnedOffFeatures.delete(accession)
4301
+ // only a departure from the accession's default is recorded
4302
+ if (shown === self.defaultOffAccessions.has(accession)) {
4303
+ self.turnedOffFeatures.set(accession, !shown)
4586
4304
  } else {
4587
- self.turnedOffFeatures.set(accession, true)
4305
+ self.turnedOffFeatures.delete(accession)
4588
4306
  }
4589
4307
  },
4590
4308
 
@@ -4593,23 +4311,28 @@ function stateModelFactory({
4593
4311
  */
4594
4312
  fit() {
4595
4313
  // two passes: each direction's viewport depends on whether the other
4596
- // overflows (the minimap takes height, the vertical scrollbar width)
4314
+ // overflows (the minimap takes height, the vertical scrollbar width).
4315
+ // Fit shrinks an alignment into the window but never grows one past
4316
+ // the default cell size, which would stretch a small alignment's rows
4317
+ // and columns out of proportion
4597
4318
  transaction(() => {
4598
4319
  for (let pass = 0; pass < 2; pass++) {
4599
- this.fitHorizontally()
4600
- this.fitVertically()
4320
+ this.fitHorizontally(defaultColWidth)
4321
+ this.fitVertically(defaultRowHeight)
4601
4322
  }
4602
4323
  })
4603
4324
  },
4604
4325
  /**
4605
4326
  * #action
4606
4327
  */
4607
- fitVertically() {
4328
+ fitVertically(maxRowHeight = maxCellSize) {
4608
4329
  if (self.numRows > 0) {
4609
- self.rowHeight = clamp(
4610
- self.msaAreaHeight / self.numRows,
4611
- minRowHeight,
4612
- maxCellSize,
4330
+ self.setRowHeight(
4331
+ clamp(
4332
+ self.msaAreaHeight / self.numRows,
4333
+ minRowHeight,
4334
+ maxRowHeight,
4335
+ ),
4613
4336
  )
4614
4337
  }
4615
4338
  self.scrollY = 0
@@ -4626,10 +4349,12 @@ function stateModelFactory({
4626
4349
  return
4627
4350
  }
4628
4351
  transaction(() => {
4629
- self.colWidth = clamp(
4630
- self.msaCanvasWidth / (span.endCol - span.startCol + 1),
4631
- minColWidth,
4632
- maxCellSize,
4352
+ self.setColWidth(
4353
+ clamp(
4354
+ self.msaCanvasWidth / (span.endCol - span.startCol + 1),
4355
+ minColWidth,
4356
+ maxCellSize,
4357
+ ),
4633
4358
  )
4634
4359
  self.scrollX = clamp(
4635
4360
  -span.startCol * self.colWidth,
@@ -4638,6 +4363,27 @@ function stateModelFactory({
4638
4363
  )
4639
4364
  })
4640
4365
  },
4366
+ /**
4367
+ * #action
4368
+ * zoomToRegion, and for a region naming a row, also scroll that row to
4369
+ * the middle of the alignment, as Go to does. What `region` opens on; a
4370
+ * caller following a range as it moves wants zoomToRegion alone, which
4371
+ * leaves the reader's vertical scroll where they put it.
4372
+ */
4373
+ revealRegion(region: Region) {
4374
+ transaction(() => {
4375
+ this.zoomToRegion(region)
4376
+ const row =
4377
+ region.row === undefined
4378
+ ? undefined
4379
+ : self.rowNamesSet.get(region.row)
4380
+ if (row !== undefined && self.viewInitialized) {
4381
+ self.setScrollY(
4382
+ -(row + 0.5) * self.rowHeight + self.msaAreaHeight / 2,
4383
+ )
4384
+ }
4385
+ })
4386
+ },
4641
4387
  /**
4642
4388
  * #action
4643
4389
  * zoom and scroll so the selected columns fill the alignment's width,
@@ -4654,10 +4400,12 @@ function stateModelFactory({
4654
4400
  const { rowRuns } = resolvedSelection
4655
4401
  const first = rowRuns[0]![0]
4656
4402
  const last = rowRuns.at(-1)![1]
4657
- self.rowHeight = clamp(
4658
- self.msaAreaHeight / (last - first + 1),
4659
- minRowHeight,
4660
- maxCellSize,
4403
+ self.setRowHeight(
4404
+ clamp(
4405
+ self.msaAreaHeight / (last - first + 1),
4406
+ minRowHeight,
4407
+ maxCellSize,
4408
+ ),
4661
4409
  )
4662
4410
  self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
4663
4411
  }
@@ -4698,13 +4446,15 @@ function stateModelFactory({
4698
4446
  /**
4699
4447
  * #action
4700
4448
  */
4701
- fitHorizontally() {
4449
+ fitHorizontally(maxColWidth = maxCellSize) {
4702
4450
  if (self.numColumns > 0) {
4703
4451
  // msaCanvasWidth excludes the vertical scrollbar's 20px
4704
- self.colWidth = clamp(
4705
- self.msaCanvasWidth / self.numColumns,
4706
- minColWidth,
4707
- maxCellSize,
4452
+ self.setColWidth(
4453
+ clamp(
4454
+ self.msaCanvasWidth / self.numColumns,
4455
+ minColWidth,
4456
+ maxColWidth,
4457
+ ),
4708
4458
  )
4709
4459
  }
4710
4460
  self.scrollX = 0
@@ -4715,21 +4465,18 @@ function stateModelFactory({
4715
4465
  self.setHighlightedColumns(self.highlightColumns)
4716
4466
  }
4717
4467
 
4718
- // The `collapse`, `focus` and `rotate` clade marks seed the collapsed
4719
- // list, the subtree in focus and the rotated nodes, which the tree,
4720
- // `hideGapsEffective` and the alignment all read. The tree arrives with
4721
- // the model for inline data and later for a filehandle, so the seeding
4722
- // waits for it and then runs once per data load, per change of
4723
- // `clades` and per reroot, which clears the node ids the seeding
4724
- // wrote: expanding a seeded clade sticks until one of those changes.
4725
- // `dataInitialized` is true once the MSA alone has loaded, when the
4726
- // tree is still the flat stub, so a tree filehandle holds the seeding
4727
- // until its text lands.
4468
+ const awaitingTree = () => !!self.treeFilehandle && !self.data.tree
4469
+
4470
+ // `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
4471
+ // and `rotated`. Seeding runs once per data load, `clades` change and
4472
+ // reroot, so expanding a seeded clade sticks until one of those. A tree
4473
+ // filehandle holds it back, since `dataInitialized` is true while the
4474
+ // tree is still the flat stub.
4728
4475
  addDisposer(
4729
4476
  self,
4730
4477
  reaction(
4731
4478
  () =>
4732
- self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
4479
+ self.dataInitialized && !awaitingTree()
4733
4480
  ? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
4734
4481
  : undefined,
4735
4482
  seed => {
@@ -4767,9 +4514,9 @@ function stateModelFactory({
4767
4514
  region &&
4768
4515
  self.viewInitialized &&
4769
4516
  self.numColumns > 0 &&
4770
- !(self.treeFilehandle && !self.data.tree)
4517
+ !awaitingTree()
4771
4518
  ) {
4772
- this.zoomToRegion(region)
4519
+ this.revealRegion(region)
4773
4520
  self.setRegion(undefined)
4774
4521
  }
4775
4522
  }),
@@ -4796,105 +4543,7 @@ function stateModelFactory({
4796
4543
  })
4797
4544
  }
4798
4545
 
4799
- /**
4800
- * Fetch a filehandle whenever it changes, and hand the text to
4801
- * `onLoad`.
4802
- *
4803
- * A generation guard keeps a slower superseded request from overwriting
4804
- * the newer one's data, status or loading flag. A superseded or cleared
4805
- * request is aborted and clears its status line.
4806
- *
4807
- * `clearFilehandle` runs after a local file loads, since a blob has no
4808
- * URL to refetch, and when the user cancels, which returns the view to
4809
- * the import form.
4810
- *
4811
- * `what` names the layer in a failure message. An `optional` layer
4812
- * (annotations, row metadata) that fails adds a warning instead of an
4813
- * error.
4814
- */
4815
- const loadOnFilehandleChange = ({
4816
- what,
4817
- getFilehandle,
4818
- onLoad,
4819
- setLoading,
4820
- clearFilehandle,
4821
- optional = false,
4822
- }: {
4823
- what: string
4824
- getFilehandle: () => FileLocationType | undefined
4825
- onLoad: (text: string) => void
4826
- setLoading?: (arg: boolean) => void
4827
- clearFilehandle?: () => void
4828
- optional?: boolean
4829
- }) => {
4830
- let generation = 0
4831
- let cancelInFlight: (() => void) | undefined
4832
- addDisposer(self, () => {
4833
- cancelInFlight?.()
4834
- })
4835
- addDisposer(
4836
- self,
4837
- autorun(async () => {
4838
- const filehandle = getFilehandle()
4839
- const current = ++generation
4840
- cancelInFlight?.()
4841
- cancelInFlight = undefined
4842
- if (!filehandle) {
4843
- setLoading?.(false)
4844
- return
4845
- }
4846
- const isCurrent = () => current === generation && isAlive(self)
4847
- const controller = new AbortController()
4848
- cancelInFlight = () => {
4849
- controller.abort()
4850
- if (isAlive(self)) {
4851
- self.setStatus(undefined)
4852
- }
4853
- }
4854
- try {
4855
- setLoading?.(true)
4856
- self.setError(undefined)
4857
- const text = await fetchTextWithProgress(
4858
- openLocation(filehandle),
4859
- status => {
4860
- if (isCurrent()) {
4861
- self.setStatus(status)
4862
- }
4863
- },
4864
- { controller },
4865
- )
4866
- if (isCurrent()) {
4867
- transaction(() => {
4868
- onLoad(text)
4869
- if (filehandle.locationType === 'BlobLocation') {
4870
- clearFilehandle?.()
4871
- }
4872
- })
4873
- }
4874
- } catch (e) {
4875
- if (isCurrent()) {
4876
- if (isAbortError(e)) {
4877
- clearFilehandle?.()
4878
- } else {
4879
- console.error(e)
4880
- if (optional) {
4881
- self.addWarning(`The ${what} did not load: ${e}`)
4882
- } else {
4883
- self.setError(e)
4884
- }
4885
- }
4886
- }
4887
- } finally {
4888
- if (isCurrent()) {
4889
- cancelInFlight = undefined
4890
- setLoading?.(false)
4891
- }
4892
- }
4893
- }),
4894
- )
4895
- }
4896
-
4897
- loadOnFilehandleChange({
4546
+ installFilehandleLoader(self, {
4898
4547
  what: 'tree',
4899
4548
  getFilehandle: () => self.treeFilehandle,
4900
4549
  onLoad: text => {
@@ -4909,7 +4558,7 @@ function stateModelFactory({
4909
4558
  })
4910
4559
 
4911
4560
  // treeMetadata has no import-form step, so no loading flag or cancel
4912
- loadOnFilehandleChange({
4561
+ installFilehandleLoader(self, {
4913
4562
  what: 'row metadata',
4914
4563
  optional: true,
4915
4564
  getFilehandle: () => self.treeMetadataFilehandle,
@@ -4941,7 +4590,7 @@ function stateModelFactory({
4941
4590
  )
4942
4591
 
4943
4592
  // gffFilehandle loads into data.gff, which the autorun above parses
4944
- loadOnFilehandleChange({
4593
+ installFilehandleLoader(self, {
4945
4594
  what: 'annotations',
4946
4595
  optional: true,
4947
4596
  getFilehandle: () => self.gffFilehandle,
@@ -4953,7 +4602,7 @@ function stateModelFactory({
4953
4602
  },
4954
4603
  })
4955
4604
 
4956
- loadOnFilehandleChange({
4605
+ installFilehandleLoader(self, {
4957
4606
  what: 'alignment',
4958
4607
  getFilehandle: () => self.msaFilehandle,
4959
4608
  onLoad: text => {
@@ -4977,20 +4626,16 @@ function stateModelFactory({
4977
4626
  self,
4978
4627
  autorun(() => {
4979
4628
  if (self.dynamicColorSchemeName) {
4980
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4981
- self.colStats
4629
+ readComputed(self.colStats)
4982
4630
  }
4983
4631
  if (self.showColumnStats) {
4984
- // everything the hover tooltip reads per column
4985
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4986
- self.colStats
4987
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4988
- self.conservation
4989
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4990
- self.propertyConservation
4632
+ readComputed(
4633
+ self.colStats,
4634
+ self.conservation,
4635
+ self.propertyConservation,
4636
+ )
4991
4637
  }
4992
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4993
- self.columns
4638
+ readComputed(self.columns)
4994
4639
  }),
4995
4640
  )
4996
4641
  },